diff --git a/README.md b/README.md new file mode 100644 index 0000000..b4fc03c --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# wson + +Rust implementation of the WSON text format and JBON binary format, with serde support. + +## Features + +- Serde API for ordinary acyclic Rust data: + - text: `wson::to_string`, `wson::from_str` + - binary JBON: `wson::to_vec`, `wson::from_slice` +- Graph API for WSON reference/cycle support: + - text references: `(0){"child":$0}` + - binary references: marker `0x06` for ids and `0x05` for references +- Java-compatible defaults from `net.woggioni:wson`: + - sorted objects (`BTreeMap`) + - parser `max_depth = 1_048_576` + - `serialize_references = false` unless enabled +- Signed zigzag LEB128 and byte-reversed double encoding compatible with `net.woggioni:jwo`. + +## Quick start + +```toml +[dependencies] +serde = { version = "1.0", features = ["derive"] } +wson = "0.1" +``` + +```rust +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +struct Config { + name: String, + enabled: bool, + retries: i64, +} + +fn main() -> wson::Result<()> { + let value = Config { + name: "worker".to_string(), + enabled: true, + retries: 3, + }; + + let text = wson::to_string(&value)?; + assert_eq!(text, r#"{"enabled":true,"name":"worker","retries":3}"#); + let decoded: Config = wson::from_str(&text)?; + assert_eq!(decoded, value); + + let bytes = wson::to_vec(&value)?; + let decoded: Config = wson::from_slice(&bytes)?; + assert_eq!(decoded, value); + + Ok(()) +} +``` + +## References and cycles + +Serde's data model is acyclic, so cyclic/shared WSON values use the graph API: + +```rust +use wson::{Config, Node}; + +fn main() -> wson::Result<()> { + let cfg = Config { + serialize_references: true, + ..Config::default() + }; + + let graph = wson::text::parse_str("(0){\"child\":$0,\"id\":25}", &cfg)?; + let root = graph.root(); + let child = match graph.node(root) { + Node::Object(entries) => *entries.get("child").unwrap(), + _ => panic!("expected object"), + }; + assert_eq!(root, child); + + let text = wson::text::dump_graph(&graph, &cfg)?; + let jbon = wson::binary::dump_graph(&graph, &cfg)?; + println!("{text} ({} JBON bytes)", jbon.len()); + Ok(()) +} +``` + +## Compatibility notes + +- WSON integers are signed 64-bit. `u64` values above `i64::MAX` fail to serialize. +- WSON floats are `f64`. Text output uses Java `Double.toString`-style formatting. +- The text parser intentionally mirrors the Java parser's leniency: commas/colons are not strictly validated, and unknown characters are skipped. +- The parser enforces `Config::max_depth`; dumpers do not limit depth, but cyclic graphs require `serialize_references`. +- Object keys are sorted by default to match the Java default `TreeMap` object implementation. + +## Examples + +```bash +cargo run --example serde_text +cargo run --example serde_jbon +cargo run --example graph_references +``` + +## Development + +```bash +cargo fmt --check +cargo clippy --all-targets -- -D warnings +cargo test +``` + +## License + +MIT diff --git a/examples/graph_references.rs b/examples/graph_references.rs new file mode 100644 index 0000000..b29b880 --- /dev/null +++ b/examples/graph_references.rs @@ -0,0 +1,34 @@ +use wson::{Config, Node}; + +fn main() -> wson::Result<()> { + let cfg = Config { + serialize_references: true, + ..Config::default() + }; + + let graph = wson::text::parse_str("(0){\"child\":$0,\"id\":25}", &cfg)?; + let root = graph.root(); + let child = match graph.node(root) { + Node::Object(entries) => *entries.get("child").expect("child entry"), + other => panic!("expected object, got {other:?}"), + }; + assert_eq!(root, child); + println!("root and child are the same node: {root:?}"); + + let text = wson::text::dump_graph(&graph, &cfg)?; + println!("text: {text}"); + + let jbon = wson::binary::dump_graph(&graph, &cfg)?; + println!("jbon length: {} bytes", jbon.len()); + + let reparsed = wson::binary::parse_slice(&jbon, &cfg)?; + let reparsed_root = reparsed.root(); + let reparsed_child = match reparsed.node(reparsed_root) { + Node::Object(entries) => *entries.get("child").expect("child entry"), + other => panic!("expected object, got {other:?}"), + }; + assert_eq!(reparsed_root, reparsed_child); + println!("binary round trip ok"); + + Ok(()) +} diff --git a/examples/serde_jbon.rs b/examples/serde_jbon.rs new file mode 100644 index 0000000..9db6ccb --- /dev/null +++ b/examples/serde_jbon.rs @@ -0,0 +1,31 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +struct ServiceConfig { + name: String, + enabled: bool, + retries: i64, + endpoints: Vec, +} + +fn main() -> wson::Result<()> { + let value = ServiceConfig { + name: "worker".to_string(), + enabled: true, + retries: 3, + endpoints: vec![ + "https://a.example".to_string(), + "https://b.example".to_string(), + ], + }; + + let bytes = wson::to_vec(&value)?; + println!("jbon length: {} bytes", bytes.len()); + println!("jbon prefix: {:02x?}", &bytes[..bytes.len().min(16)]); + + let decoded: ServiceConfig = wson::from_slice(&bytes)?; + assert_eq!(decoded, value); + println!("round trip ok"); + + Ok(()) +} diff --git a/examples/serde_text.rs b/examples/serde_text.rs new file mode 100644 index 0000000..f2d0e01 --- /dev/null +++ b/examples/serde_text.rs @@ -0,0 +1,30 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +struct ServiceConfig { + name: String, + enabled: bool, + retries: i64, + endpoints: Vec, +} + +fn main() -> wson::Result<()> { + let value = ServiceConfig { + name: "worker".to_string(), + enabled: true, + retries: 3, + endpoints: vec![ + "https://a.example".to_string(), + "https://b.example".to_string(), + ], + }; + + let text = wson::to_string(&value)?; + println!("text: {text}"); + + let decoded: ServiceConfig = wson::from_str(&text)?; + assert_eq!(decoded, value); + println!("round trip ok"); + + Ok(()) +}