Initial Rust WSON serde implementation
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
use super::float_fmt::format_java_double;
|
||||
use crate::config::Config;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::graph::{Graph, Node, NodeId};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::Write;
|
||||
|
||||
fn escape_string(value: &str, out: &mut String) {
|
||||
out.push('"');
|
||||
for c in value.chars() {
|
||||
match c {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
_ => {
|
||||
if (c as u32) < 128 {
|
||||
out.push(c);
|
||||
} else if (c as u32) <= 0xffff {
|
||||
out.push_str(&format!("\\u{:04X}", c as u32));
|
||||
} else {
|
||||
// Rust strings cannot contain the UTF-16 surrogate halves Java emits.
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
}
|
||||
|
||||
fn count_occurrences(graph: &Graph) -> HashMap<NodeId, usize> {
|
||||
fn visit(graph: &Graph, id: NodeId, counts: &mut HashMap<NodeId, usize>) {
|
||||
let count = counts.entry(id).or_insert(0);
|
||||
*count += 1;
|
||||
if *count != 1 {
|
||||
return;
|
||||
}
|
||||
match graph.node(id) {
|
||||
Node::Array(items) => {
|
||||
for child in items {
|
||||
visit(graph, *child, counts);
|
||||
}
|
||||
}
|
||||
Node::Object(entries) => {
|
||||
for child in entries.values() {
|
||||
visit(graph, *child, counts);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut counts = HashMap::new();
|
||||
visit(graph, graph.root(), &mut counts);
|
||||
counts
|
||||
}
|
||||
|
||||
fn assign_ids(graph: &Graph, counts: &HashMap<NodeId, usize>) -> HashMap<NodeId, usize> {
|
||||
fn visit(
|
||||
graph: &Graph,
|
||||
id: NodeId,
|
||||
counts: &HashMap<NodeId, usize>,
|
||||
seen: &mut HashSet<NodeId>,
|
||||
ids: &mut HashMap<NodeId, usize>,
|
||||
) {
|
||||
if counts.get(&id).copied().unwrap_or(0) > 1 && !ids.contains_key(&id) {
|
||||
let next = ids.len();
|
||||
ids.insert(id, next);
|
||||
}
|
||||
if !seen.insert(id) {
|
||||
return;
|
||||
}
|
||||
match graph.node(id) {
|
||||
Node::Array(items) => {
|
||||
for child in items {
|
||||
visit(graph, *child, counts, seen, ids);
|
||||
}
|
||||
}
|
||||
Node::Object(entries) => {
|
||||
for child in entries.values() {
|
||||
visit(graph, *child, counts, seen, ids);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut ids = HashMap::new();
|
||||
let mut seen = HashSet::new();
|
||||
visit(graph, graph.root(), counts, &mut seen, &mut ids);
|
||||
ids
|
||||
}
|
||||
|
||||
struct DumpState<'a> {
|
||||
graph: &'a Graph,
|
||||
cfg: &'a Config,
|
||||
ids: HashMap<NodeId, usize>,
|
||||
dumped: HashSet<NodeId>,
|
||||
visiting: HashSet<NodeId>,
|
||||
}
|
||||
|
||||
impl<'a> DumpState<'a> {
|
||||
fn new(graph: &'a Graph, cfg: &'a Config) -> Self {
|
||||
let ids = if cfg.serialize_references {
|
||||
let counts = count_occurrences(graph);
|
||||
assign_ids(graph, &counts)
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
Self {
|
||||
graph,
|
||||
cfg,
|
||||
ids,
|
||||
dumped: HashSet::new(),
|
||||
visiting: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dump_node(&mut self, id: NodeId, out: &mut String) -> Result<()> {
|
||||
let node = self.graph.node(id);
|
||||
if self.cfg.serialize_references {
|
||||
if let Some(reference_id) = self.ids.get(&id).copied() {
|
||||
if !self.dumped.insert(id) {
|
||||
out.push('$');
|
||||
out.push_str(&reference_id.to_string());
|
||||
return Ok(());
|
||||
}
|
||||
out.push('(');
|
||||
out.push_str(&reference_id.to_string());
|
||||
out.push(')');
|
||||
}
|
||||
}
|
||||
|
||||
match node {
|
||||
Node::Null => out.push_str("null"),
|
||||
Node::Bool(value) => out.push_str(if *value { "true" } else { "false" }),
|
||||
Node::Integer(value) => out.push_str(&value.to_string()),
|
||||
Node::Float(value) => out.push_str(&format_java_double(*value)),
|
||||
Node::String(value) => escape_string(value, out),
|
||||
Node::Array(items) => {
|
||||
if !self.cfg.serialize_references && !self.visiting.insert(id) {
|
||||
return Err(Error::Cycle);
|
||||
}
|
||||
out.push('[');
|
||||
for (index, child) in items.iter().enumerate() {
|
||||
if index > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
self.dump_node(*child, out)?;
|
||||
}
|
||||
out.push(']');
|
||||
self.visiting.remove(&id);
|
||||
}
|
||||
Node::Object(entries) => {
|
||||
if !self.cfg.serialize_references && !self.visiting.insert(id) {
|
||||
return Err(Error::Cycle);
|
||||
}
|
||||
out.push('{');
|
||||
for (index, (key, child)) in entries.iter().enumerate() {
|
||||
if index > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
escape_string(key, out);
|
||||
out.push(':');
|
||||
self.dump_node(*child, out)?;
|
||||
}
|
||||
out.push('}');
|
||||
self.visiting.remove(&id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dump_graph(graph: &Graph, cfg: &Config) -> Result<String> {
|
||||
let mut out = String::new();
|
||||
DumpState::new(graph, cfg).dump_node(graph.root(), &mut out)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn dump_graph_to_writer<W: Write>(graph: &Graph, cfg: &Config, mut writer: W) -> Result<()> {
|
||||
let text = dump_graph(graph, cfg)?;
|
||||
writer.write_all(text.as_bytes())?;
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::text::parser::parse_str;
|
||||
|
||||
#[test]
|
||||
fn compact_round_trip() {
|
||||
let cfg = Config::default();
|
||||
let graph = parse_str("{\"b\": [1, 2.5, null], \"a\": true}", &cfg).unwrap();
|
||||
assert_eq!(
|
||||
dump_graph(&graph, &cfg).unwrap(),
|
||||
"{\"a\":true,\"b\":[1,2.5,null]}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/// Formats `f64` with Java `Double.toString` semantics as closely as Rust's
|
||||
/// shortest-roundtrip formatting allows.
|
||||
pub fn format_java_double(value: f64) -> String {
|
||||
if value.is_nan() {
|
||||
return "NaN".to_string();
|
||||
}
|
||||
if value == f64::INFINITY {
|
||||
return "Infinity".to_string();
|
||||
}
|
||||
if value == f64::NEG_INFINITY {
|
||||
return "-Infinity".to_string();
|
||||
}
|
||||
if value == 0.0 {
|
||||
return if value.is_sign_negative() {
|
||||
"-0.0".to_string()
|
||||
} else {
|
||||
"0.0".to_string()
|
||||
};
|
||||
}
|
||||
|
||||
let scientific = format!("{value:e}");
|
||||
let (mantissa, exp) = scientific
|
||||
.split_once('e')
|
||||
.expect("lowerexp formatting always contains e");
|
||||
let exponent: i32 = exp.parse().expect("lowerexp exponent is an integer");
|
||||
let negative = mantissa.starts_with('-');
|
||||
let mantissa = mantissa.trim_start_matches('-');
|
||||
let digits: String = mantissa.chars().filter(|c| *c != '.').collect();
|
||||
|
||||
// Java uses plain decimal notation for 1e-3 <= abs(value) < 1e7.
|
||||
if (-3..7).contains(&exponent) {
|
||||
let point_pos = exponent + 1;
|
||||
let mut out = String::new();
|
||||
if negative {
|
||||
out.push('-');
|
||||
}
|
||||
if point_pos <= 0 {
|
||||
out.push_str("0.");
|
||||
for _ in 0..(-point_pos) {
|
||||
out.push('0');
|
||||
}
|
||||
out.push_str(&digits);
|
||||
} else if point_pos as usize >= digits.len() {
|
||||
out.push_str(&digits);
|
||||
for _ in 0..(point_pos as usize - digits.len()) {
|
||||
out.push('0');
|
||||
}
|
||||
out.push_str(".0");
|
||||
} else {
|
||||
let split = point_pos as usize;
|
||||
out.push_str(&digits[..split]);
|
||||
out.push('.');
|
||||
out.push_str(&digits[split..]);
|
||||
}
|
||||
out
|
||||
} else {
|
||||
let mut out = String::new();
|
||||
if negative {
|
||||
out.push('-');
|
||||
}
|
||||
out.push(digits.as_bytes()[0] as char);
|
||||
out.push('.');
|
||||
if digits.len() == 1 {
|
||||
out.push('0');
|
||||
} else {
|
||||
out.push_str(&digits[1..]);
|
||||
}
|
||||
out.push('E');
|
||||
out.push_str(&exponent.to_string());
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::format_java_double;
|
||||
|
||||
#[test]
|
||||
fn java_double_formatting() {
|
||||
assert_eq!(format_java_double(1.0), "1.0");
|
||||
assert_eq!(format_java_double(36.0), "36.0");
|
||||
assert_eq!(format_java_double(0.001), "0.001");
|
||||
assert_eq!(format_java_double(0.0001), "1.0E-4");
|
||||
assert_eq!(format_java_double(9_999_999.0), "9999999.0");
|
||||
assert_eq!(format_java_double(10_000_000.0), "1.0E7");
|
||||
assert_eq!(format_java_double(1e22), "1.0E22");
|
||||
assert_eq!(format_java_double(1e-7), "1.0E-7");
|
||||
assert_eq!(format_java_double(-2.5), "-2.5");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
pub mod dumper;
|
||||
pub mod float_fmt;
|
||||
pub mod parser;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::de::ValueDeserializer;
|
||||
use crate::error::Result;
|
||||
use crate::graph::Graph;
|
||||
use crate::ser::ValueSerializer;
|
||||
use crate::value::Value;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{Read, Write};
|
||||
|
||||
pub fn parse_str(input: &str, cfg: &Config) -> Result<Graph> {
|
||||
parser::parse_str(input, cfg)
|
||||
}
|
||||
|
||||
pub fn parse_reader<R: Read>(reader: R, cfg: &Config) -> Result<Graph> {
|
||||
parser::parse_reader(reader, cfg)
|
||||
}
|
||||
|
||||
pub fn dump_graph(graph: &Graph, cfg: &Config) -> Result<String> {
|
||||
dumper::dump_graph(graph, cfg)
|
||||
}
|
||||
|
||||
pub fn dump_graph_to_writer<W: Write>(graph: &Graph, cfg: &Config, writer: W) -> Result<()> {
|
||||
dumper::dump_graph_to_writer(graph, cfg, writer)
|
||||
}
|
||||
|
||||
pub fn to_string<T: ?Sized + Serialize>(value: &T) -> Result<String> {
|
||||
to_string_with_config(value, &Config::default())
|
||||
}
|
||||
|
||||
pub fn to_string_with_config<T: ?Sized + Serialize>(value: &T, cfg: &Config) -> Result<String> {
|
||||
let value = value.serialize(ValueSerializer)?;
|
||||
let graph = Graph::from_value(&value);
|
||||
dumper::dump_graph(&graph, cfg)
|
||||
}
|
||||
|
||||
pub fn to_writer<T: ?Sized + Serialize, W: Write>(value: &T, writer: W) -> Result<()> {
|
||||
to_writer_with_config(value, &Config::default(), writer)
|
||||
}
|
||||
|
||||
pub fn to_writer_with_config<T: ?Sized + Serialize, W: Write>(
|
||||
value: &T,
|
||||
cfg: &Config,
|
||||
mut writer: W,
|
||||
) -> Result<()> {
|
||||
let text = to_string_with_config(value, cfg)?;
|
||||
writer.write_all(text.as_bytes())?;
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn from_str<'a, T: Deserialize<'a>>(input: &'a str) -> Result<T> {
|
||||
from_str_with_config(input, &Config::default())
|
||||
}
|
||||
|
||||
pub fn from_str_with_config<'a, T: Deserialize<'a>>(input: &'a str, cfg: &Config) -> Result<T> {
|
||||
let graph = parser::parse_str(input, cfg)?;
|
||||
let value = Value::from_graph(&graph)?;
|
||||
T::deserialize(ValueDeserializer::new(value))
|
||||
}
|
||||
|
||||
pub fn from_reader<'a, T: Deserialize<'a>, R: Read>(reader: R) -> Result<T> {
|
||||
from_reader_with_config(reader, &Config::default())
|
||||
}
|
||||
|
||||
pub fn from_reader_with_config<'a, T: Deserialize<'a>, R: Read>(
|
||||
reader: R,
|
||||
cfg: &Config,
|
||||
) -> Result<T> {
|
||||
let graph = parser::parse_reader(reader, cfg)?;
|
||||
let value = Value::from_graph(&graph)?;
|
||||
T::deserialize(ValueDeserializer::new(value))
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
use crate::config::Config;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::graph::{ContainerKind, Graph, Node, ParseStack};
|
||||
use std::io::Read;
|
||||
|
||||
pub(crate) struct TextParser<'a> {
|
||||
chars: Vec<char>,
|
||||
index: usize,
|
||||
current: Option<char>,
|
||||
line: usize,
|
||||
column: usize,
|
||||
cfg: &'a Config,
|
||||
}
|
||||
|
||||
impl<'a> TextParser<'a> {
|
||||
pub fn new(input: &str, cfg: &'a Config) -> Self {
|
||||
let mut parser = Self {
|
||||
chars: input.chars().collect(),
|
||||
index: 0,
|
||||
current: None,
|
||||
line: 1,
|
||||
column: 1,
|
||||
cfg,
|
||||
};
|
||||
parser.advance();
|
||||
parser
|
||||
}
|
||||
|
||||
fn advance(&mut self) -> Option<char> {
|
||||
if self.index >= self.chars.len() {
|
||||
self.current = None;
|
||||
return None;
|
||||
}
|
||||
let c = self.chars[self.index];
|
||||
self.index += 1;
|
||||
self.current = Some(c);
|
||||
if c == '\n' {
|
||||
self.line += 1;
|
||||
self.column = 1;
|
||||
} else {
|
||||
self.column += 1;
|
||||
}
|
||||
Some(c)
|
||||
}
|
||||
|
||||
fn error<T>(&self, message: impl Into<String>) -> Result<T> {
|
||||
Err(Error::text(self.line, self.column, message))
|
||||
}
|
||||
|
||||
fn is_blank(c: char) -> bool {
|
||||
matches!(c, ' ' | '\t' | '\n' | '\r')
|
||||
}
|
||||
|
||||
fn is_decimal(c: char) -> bool {
|
||||
matches!(c, '0'..='9' | '+' | '-' | '.' | 'e')
|
||||
}
|
||||
|
||||
fn parse_hex(&mut self) -> u32 {
|
||||
let mut result = 0u32;
|
||||
while let Some(c) = self.current {
|
||||
let digit = match c {
|
||||
'0'..='9' => c as u32 - '0' as u32,
|
||||
'a'..='f' => 10 + c as u32 - 'a' as u32,
|
||||
'A'..='F' => 10 + c as u32 - 'A' as u32,
|
||||
_ => break,
|
||||
};
|
||||
result = (result << 4).wrapping_add(digit);
|
||||
self.advance();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn parse_number_text(&mut self) -> String {
|
||||
let mut out = String::new();
|
||||
while let Some(c) = self.current {
|
||||
if !Self::is_decimal(c) {
|
||||
break;
|
||||
}
|
||||
out.push(c);
|
||||
self.advance();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn parse_id(&mut self) -> Result<i64> {
|
||||
let mut out = String::new();
|
||||
let mut digits_started = false;
|
||||
let mut digits_ended = false;
|
||||
while let Some(c) = self.current {
|
||||
if c == '(' {
|
||||
// ignored
|
||||
} else if c.is_whitespace() {
|
||||
if digits_started {
|
||||
digits_ended = true;
|
||||
}
|
||||
} else if c == ')' {
|
||||
break;
|
||||
} else if Self::is_decimal(c) {
|
||||
if digits_ended {
|
||||
return self.error("error parsing id");
|
||||
}
|
||||
digits_started = true;
|
||||
out.push(c);
|
||||
}
|
||||
self.advance();
|
||||
}
|
||||
out.parse::<i64>()
|
||||
.map_err(|err| Error::text(self.line, self.column, err.to_string()))
|
||||
}
|
||||
|
||||
fn read_string(&mut self) -> Result<String> {
|
||||
// current is the opening quote
|
||||
self.advance();
|
||||
let mut out = String::new();
|
||||
while let Some(c) = self.current {
|
||||
match c {
|
||||
'"' => {
|
||||
self.advance();
|
||||
break;
|
||||
}
|
||||
'\\' => {
|
||||
self.advance();
|
||||
let Some(escaped) = self.current else {
|
||||
break;
|
||||
};
|
||||
match escaped {
|
||||
'"' => out.push('"'),
|
||||
'r' => out.push('\r'),
|
||||
'n' => out.push('\n'),
|
||||
't' => out.push('\t'),
|
||||
'\\' => out.push('\\'),
|
||||
'u' => {
|
||||
self.advance();
|
||||
let code_point = self.parse_hex();
|
||||
match char::from_u32(code_point) {
|
||||
Some(ch) => out.push(ch),
|
||||
None => return self.error("invalid unicode escape"),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
other => {
|
||||
return self.error(format!("Unrecognized escape sequence '\\{other}'"))
|
||||
}
|
||||
}
|
||||
self.advance();
|
||||
}
|
||||
_ => {
|
||||
out.push(c);
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn consume_expected(&mut self, expected: &str, error_message: &str) -> Result<()> {
|
||||
for expected_char in expected.chars() {
|
||||
match self.current {
|
||||
None => return self.error("Unexpected end of stream"),
|
||||
Some(c) if c == expected_char => {
|
||||
self.advance();
|
||||
}
|
||||
_ => return self.error(error_message),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse(mut self) -> Result<Graph> {
|
||||
let mut stack = ParseStack::new(self.cfg);
|
||||
let mut current_id: Option<i64> = None;
|
||||
|
||||
while let Some(c) = self.current {
|
||||
if Self::is_blank(c) {
|
||||
self.advance();
|
||||
} else if c == '(' && self.cfg.serialize_references {
|
||||
current_id = Some(self.parse_id()?);
|
||||
} else if c == '{' {
|
||||
let node = stack.begin_object(None)?;
|
||||
if let Some(id) = current_id.take() {
|
||||
stack.register_id(id, node);
|
||||
}
|
||||
self.advance();
|
||||
} else if c == '}' {
|
||||
stack.end_object().map_err(|err| match err {
|
||||
Error::Message(msg) => Error::text(self.line, self.column, msg),
|
||||
other => other,
|
||||
})?;
|
||||
self.advance();
|
||||
} else if c == '[' {
|
||||
let node = stack.begin_array(None)?;
|
||||
if let Some(id) = current_id.take() {
|
||||
stack.register_id(id, node);
|
||||
}
|
||||
self.advance();
|
||||
} else if c == ']' {
|
||||
stack.end_array().map_err(|err| match err {
|
||||
Error::Message(msg) => Error::text(self.line, self.column, msg),
|
||||
other => other,
|
||||
})?;
|
||||
self.advance();
|
||||
} else if Self::is_decimal(c) {
|
||||
let text = self.parse_number_text();
|
||||
let node = if text.find('.').is_some_and(|pos| pos > 0) {
|
||||
let value = text
|
||||
.parse::<f64>()
|
||||
.map_err(|err| Error::text(self.line, self.column, err.to_string()))?;
|
||||
stack.builder.add(Node::Float(value))
|
||||
} else {
|
||||
let value = text
|
||||
.parse::<i64>()
|
||||
.map_err(|err| Error::text(self.line, self.column, err.to_string()))?;
|
||||
stack.builder.add(Node::Integer(value))
|
||||
};
|
||||
stack.add_value(node);
|
||||
continue;
|
||||
} else if c == '"' {
|
||||
let text = self.read_string()?;
|
||||
if stack.expecting_object_key() {
|
||||
stack.object_key(text);
|
||||
} else {
|
||||
let node = stack.builder.add(Node::String(text));
|
||||
stack.add_value(node);
|
||||
}
|
||||
continue;
|
||||
} else if c == 't' {
|
||||
self.consume_expected("true", "Unrecognized boolean value")?;
|
||||
let node = stack.builder.add(Node::Bool(true));
|
||||
stack.add_value(node);
|
||||
continue;
|
||||
} else if c == 'f' {
|
||||
self.consume_expected("false", "Unrecognized boolean value")?;
|
||||
let node = stack.builder.add(Node::Bool(false));
|
||||
stack.add_value(node);
|
||||
continue;
|
||||
} else if c == 'n' {
|
||||
self.consume_expected("null", "Unrecognized null value")?;
|
||||
let node = stack.builder.add(Node::Null);
|
||||
stack.add_value(node);
|
||||
continue;
|
||||
} else if c == '$' && stack.id_map.is_some() {
|
||||
self.advance();
|
||||
let text = self.parse_number_text();
|
||||
let id = text
|
||||
.parse::<i64>()
|
||||
.map_err(|err| Error::text(self.line, self.column, err.to_string()))?;
|
||||
let node = stack
|
||||
.resolve_reference(id)
|
||||
.map_err(|err| Error::text(self.line, self.column, err.to_string()))?;
|
||||
stack.add_value(node);
|
||||
continue;
|
||||
} else {
|
||||
// The Java parser is lenient: commas, colons and unknown characters are skipped.
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
|
||||
if stack.depth() > 1 {
|
||||
let missing = match stack.top().kind {
|
||||
ContainerKind::Array => ']',
|
||||
ContainerKind::Object => '}',
|
||||
};
|
||||
return self.error(format!("Missing '{missing}' token"));
|
||||
}
|
||||
stack.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_str(input: &str, cfg: &Config) -> Result<Graph> {
|
||||
TextParser::new(input, cfg).parse()
|
||||
}
|
||||
|
||||
pub fn parse_reader<R: Read>(mut reader: R, cfg: &Config) -> Result<Graph> {
|
||||
let mut input = String::new();
|
||||
reader.read_to_string(&mut input)?;
|
||||
parse_str(&input, cfg)
|
||||
}
|
||||
Reference in New Issue
Block a user