jsonx 0.1.0

A serde-enabled implementation of the JSONX extended-JSON format: typed value constructors, unquoted keys, and trailing commas.
Documentation
//! Open extension API: render your own types as JSONX `name(value)`
//! constructors.
//!
//! JSONX is deliberately open. A document is a valid ES5 expression, so
//! `myType(value)` is just a function call — given a definition of `myType`,
//! any constructor can be added. This module lets you teach the jsonx
//! serializer and deserializer a new constructor for one of *your* types, while
//! every other serde format still sees a plain, transparent value.
//!
//! It is the same mechanism the built-in [`ip`](crate::ip) /
//! [`ipport`](crate::ipport) / [`datetime`](crate::datetime) types use: an
//! extended value travels through serde as a newtype struct whose name is a
//! sentinel-encoded constructor name (produced by the [`ctor!`](crate::ctor)
//! macro). Our serializer turns it into `name(value)`; other serializers ignore
//! the name and emit the inner value transparently.
//!
//! # The quick way: `#[derive(JsonxConstructor)]`
//!
//! If your type already implements [`Display`](std::fmt::Display) and
//! [`FromStr`](std::str::FromStr), the [derive](macro@crate::JsonxConstructor)
//! generates everything — the trait impl *and* the serde impls — from those.
//! The constructor name defaults to the type name lowercased; override it with
//! `#[jsonx(name = "...")]`. (Requires the default `derive` feature; see the
//! [derive macro](macro@crate::JsonxConstructor) for a runnable example.)
//!
//! # The manual way: implement [`JsonxConstructor`] by hand
//!
//! When a string `Display`/`FromStr` representation doesn't fit, implement
//! [`JsonxConstructor`] directly, then delegate serde to this module's
//! [`serialize`]/[`deserialize`] helpers:
//!
//! ```
//! use jsonx::JsonxConstructor;
//!
//! #[derive(Debug, PartialEq)]
//! struct Color(u32); // an RGB triple, e.g. 0xff8800
//!
//! impl JsonxConstructor for Color {
//!     const TOKEN: &'static str = jsonx::ctor!("color");
//!     fn to_jsonx_arg(&self) -> String {
//!         format!("#{:06x}", self.0)
//!     }
//!     fn from_jsonx_arg(arg: &str) -> Result<Self, String> {
//!         let hex = arg.strip_prefix('#').unwrap_or(arg);
//!         u32::from_str_radix(hex, 16).map(Color).map_err(|e| e.to_string())
//!     }
//! }
//!
//! impl serde::Serialize for Color {
//!     fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
//!         jsonx::constructor::serialize(self, s)
//!     }
//! }
//! impl<'de> serde::Deserialize<'de> for Color {
//!     fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
//!         jsonx::constructor::deserialize(d)
//!     }
//! }
//!
//! let text = jsonx::to_string(&Color(0xff8800)).unwrap();
//! assert_eq!(text, r##"color("#ff8800")"##);
//! assert_eq!(jsonx::from_str::<Color>(&text).unwrap(), Color(0xff8800));
//! ```
//!
//! Prefer not to write the serde impls? Apply this module through serde's
//! `with` attribute instead — it dispatches through [`JsonxConstructor`] too:
//!
//! ```
//! # use jsonx::JsonxConstructor;
//! # #[derive(Debug, PartialEq)]
//! # struct Color(u32);
//! # impl JsonxConstructor for Color {
//! #     const TOKEN: &'static str = jsonx::ctor!("color");
//! #     fn to_jsonx_arg(&self) -> String { format!("#{:06x}", self.0) }
//! #     fn from_jsonx_arg(arg: &str) -> Result<Self, String> {
//! #         u32::from_str_radix(arg.strip_prefix('#').unwrap_or(arg), 16).map(Color).map_err(|e| e.to_string())
//! #     }
//! # }
//! #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
//! struct Paint {
//!     #[serde(with = "jsonx::constructor")]
//!     fill: Color,
//! }
//! ```
//!
//! # Foreign types
//!
//! Rust's orphan rule means you can only implement [`JsonxConstructor`] for a
//! type defined in your own crate. For a foreign type (e.g. `uuid::Uuid`), wrap
//! it in a newtype you own and implement the trait on the wrapper — exactly how
//! [`Ip`](crate::Ip) wraps [`std::net::IpAddr`].

use std::fmt;

use serde::de::{self, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};

/// Encodes a constructor `name` into the `&'static str` token the jsonx
/// serializer recognizes. Use it to set [`JsonxConstructor::TOKEN`].
///
/// ```
/// const TOKEN: &str = jsonx::ctor!("mytype");
/// assert!(TOKEN.ends_with("mytype"));
/// ```
#[macro_export]
macro_rules! ctor {
    ($name:literal) => {
        concat!("\u{1}jx\u{1}", $name)
    };
}

/// A type that has a JSONX `name(value)` constructor form.
///
/// The value's textual argument is a string: `to_jsonx_arg` renders it and
/// `from_jsonx_arg` parses it back. In JSONX it serializes as
/// `name("argument")`; in every other serde format it stays a plain string.
///
/// See the [module docs](self) for a worked example.
pub trait JsonxConstructor: Sized {
    /// The constructor token. Always build it with the [`ctor!`](crate::ctor)
    /// macro: `const TOKEN: &'static str = jsonx::ctor!("mytype");`.
    const TOKEN: &'static str;

    /// Renders the value as the string written inside the parentheses.
    fn to_jsonx_arg(&self) -> String;

    /// Parses the value from the string written inside the parentheses.
    fn from_jsonx_arg(arg: &str) -> Result<Self, String>;
}

/// Low-level: serialize `repr` as the body of a JSONX `name(...)` constructor,
/// where `token` is a [`ctor!`](crate::ctor)-encoded name.
///
/// Most callers should implement [`JsonxConstructor`] and use [`serialize`]
/// instead; reach for this only when the argument is not a `String`.
pub fn serialize_constructor<S, T>(
    serializer: S,
    token: &'static str,
    repr: &T,
) -> Result<S::Ok, S::Error>
where
    S: Serializer,
    T: Serialize + ?Sized,
{
    serializer.serialize_newtype_struct(token, repr)
}

/// Low-level: read the string argument of a JSONX `name(...)` constructor,
/// where `token` is a [`ctor!`](crate::ctor)-encoded name. Also accepts a bare
/// string, so values round-trip through non-JSONX formats.
pub fn deserialize_constructor<'de, D>(
    deserializer: D,
    token: &'static str,
) -> Result<String, D::Error>
where
    D: Deserializer<'de>,
{
    struct ArgVisitor;
    impl<'de> Visitor<'de> for ArgVisitor {
        type Value = String;
        fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            f.write_str("a JSONX type constructor argument")
        }
        fn visit_str<E: de::Error>(self, v: &str) -> Result<String, E> {
            Ok(v.to_owned())
        }
        fn visit_string<E: de::Error>(self, v: String) -> Result<String, E> {
            Ok(v)
        }
        fn visit_newtype_struct<D: Deserializer<'de>>(self, d: D) -> Result<String, D::Error> {
            String::deserialize(d)
        }
    }
    deserializer.deserialize_newtype_struct(token, ArgVisitor)
}

/// Serializes a [`JsonxConstructor`] value. Usable on its own or through
/// `#[serde(with = "jsonx::constructor")]`.
pub fn serialize<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
    T: JsonxConstructor,
{
    serialize_constructor(serializer, T::TOKEN, &value.to_jsonx_arg())
}

/// Deserializes a [`JsonxConstructor`] value. Usable on its own or through
/// `#[serde(with = "jsonx::constructor")]`.
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
    D: Deserializer<'de>,
    T: JsonxConstructor,
{
    let arg = deserialize_constructor(deserializer, T::TOKEN)?;
    T::from_jsonx_arg(&arg).map_err(de::Error::custom)
}

#[cfg(test)]
mod tests {
    #[test]
    fn macro_matches_internal_tokens() {
        // The public `ctor!` macro must produce exactly the encoding the
        // serializer/deserializer recognize for the built-in constructors.
        assert_eq!(crate::ctor!("int"), crate::tokens::TOKEN_INT);
        assert_eq!(crate::ctor!("uint"), crate::tokens::TOKEN_UINT);
        assert_eq!(crate::ctor!("datetime"), crate::tokens::TOKEN_DATETIME);
        assert_eq!(crate::ctor!("ip"), crate::tokens::TOKEN_IP);
        assert_eq!(crate::ctor!("ipport"), crate::tokens::TOKEN_IPPORT);
    }
}