jsonx 0.1.0

A serde-enabled implementation of the JSONX extended-JSON format: typed value constructors, unquoted keys, and trailing commas.
Documentation
//! Run with: `cargo run --example demo`

use jsonx::{Bytes, DateTime, Datetime, Ip, Value};
use std::fmt;
use std::str::FromStr;

fn main() {
    // 1. Parse a relaxed JSONX document into a dynamic `Value`. Note the
    //    unquoted keys, typed constructors, and trailing comma. (JSONX, like
    //    JSON, does not allow comments.)
    let input = r#"
        {
            service: "billing",
            replicas: int(3),
            port: uint16(8080),
            started: datetime("2024-01-02T15:04:05Z"),
            upstream: ip("10.0.0.7"),
            payload: bytes("aGVsbG8="),
            flags: [true, false,],
        }
    "#;

    let value: Value = jsonx::from_str(input).expect("valid jsonx");
    println!("parsed id replicas = {:?}", value.get("replicas"));

    // 2. Re-encode it: compact (key-sorted) and pretty.
    println!("\ncompact:\n{}", jsonx::to_string(&value).unwrap());
    println!("\npretty:\n{}", jsonx::to_string_pretty(&value).unwrap());

    // 3. Round-trip a strongly-typed struct. No `#[serde(with = ...)]`: the
    //    `Ip` / `Datetime` wrappers carry the JSONX type selection themselves,
    //    so the same struct also serializes cleanly to any other serde format.
    #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
    struct Endpoint {
        host: Ip,
        port: u16,
        secret: Bytes,
        opened: Datetime,
    }

    let endpoint = Endpoint {
        host: Ip("192.168.1.42".parse().unwrap()),
        port: 443,
        secret: Bytes(b"s3cr3t".to_vec()),
        opened: Datetime(DateTime::parse_from_rfc3339("2024-06-01T09:00:00+02:00").unwrap()),
    };

    let text = jsonx::to_string(&endpoint).unwrap();
    println!("\nendpoint encoded:\n{text}");
    let decoded: Endpoint = jsonx::from_str(&text).unwrap();
    assert_eq!(decoded, endpoint);
    println!("round-trip ok: {decoded:?}");

    // 4. JSONX is open: teach it a constructor for one of your own types. If the
    //    type already implements `Display` + `FromStr`, `#[derive(JsonxConstructor)]`
    //    is all it takes — it generates the trait and the serde impls. The value
    //    renders as `semver("...")` here and stays a plain string elsewhere.
    #[derive(jsonx::JsonxConstructor, Debug, PartialEq)]
    #[jsonx(name = "semver")]
    struct Version(u16, u16);

    impl fmt::Display for Version {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "{}.{}", self.0, self.1)
        }
    }
    impl FromStr for Version {
        type Err = String;
        fn from_str(s: &str) -> Result<Self, String> {
            let (a, b) = s.split_once('.').ok_or("expected MAJOR.MINOR")?;
            Ok(Version(
                a.parse().map_err(|_| "bad major")?,
                b.parse().map_err(|_| "bad minor")?,
            ))
        }
    }

    let text = jsonx::to_string(&Version(1, 4)).unwrap();
    println!("\ncustom constructor: {text}");
    assert_eq!(text, r#"semver("1.4")"#);
    assert_eq!(jsonx::from_str::<Version>(&text).unwrap(), Version(1, 4));
    println!("\nall good");
}