Initial Rust WSON serde implementation

This commit is contained in:
2026-09-05 23:22:25 +00:00
commit d4b77ddcb9
22 changed files with 3691 additions and 0 deletions
+201
View File
@@ -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]}"
);
}
}
+90
View File
@@ -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");
}
}
+76
View File
@@ -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))
}
+277
View File
@@ -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)
}