commit 99beb4a5385f52e7b353501b0ee08f0e6855d8f2 from: Murilo Ijanc date: Sat Apr 18 12:10:04 2026 UTC add ToJson/FromJson traits and json_struct! macro commit - 610939e0b14ba1d0c5727058de651ce1afa0e10d commit + 99beb4a5385f52e7b353501b0ee08f0e6855d8f2 blob - c57bddd73ec75cfe62db167973aa379b968ae268 blob + 89d9655a02f2cd1a90920cd514f0428a57739453 --- jackson.rs +++ jackson.rs @@ -38,7 +38,7 @@ pub struct Error { } impl Error { - const fn new(msg: &'static str, pos: usize) -> Self { + pub const fn new(msg: &'static str, pos: usize) -> Self { Self { msg, pos } } @@ -498,6 +498,211 @@ impl<'a> Parser<'a> { } } +/// Trait implemented by types that can be written to a [`Value`]. +pub trait ToJson { + fn to_json(&self) -> Value; +} + +/// Trait implemented by types that can be read from a [`Value`]. +pub trait FromJson: Sized { + fn from_json(v: &Value) -> Result; + + /// Produce a value when the corresponding field is absent from the + /// parent object. The default rejects; `Option` overrides to + /// return `None`. + fn missing_field() -> Result { + Err(Error::new("missing field", 0)) + } +} + +impl ToJson for bool { + fn to_json(&self) -> Value { + Value::Bool(*self) + } +} + +impl FromJson for bool { + fn from_json(v: &Value) -> Result { + v.as_bool().ok_or_else(|| Error::new("expected bool", 0)) + } +} + +impl ToJson for String { + fn to_json(&self) -> Value { + Value::String(self.clone()) + } +} + +impl FromJson for String { + fn from_json(v: &Value) -> Result { + v.as_str() + .map(str::to_string) + .ok_or_else(|| Error::new("expected string", 0)) + } +} + +impl ToJson for f64 { + fn to_json(&self) -> Value { + Value::Number(*self) + } +} + +impl FromJson for f64 { + fn from_json(v: &Value) -> Result { + v.as_number() + .ok_or_else(|| Error::new("expected number", 0)) + } +} + +impl ToJson for f32 { + fn to_json(&self) -> Value { + Value::Number(*self as f64) + } +} + +impl FromJson for f32 { + fn from_json(v: &Value) -> Result { + v.as_number() + .map(|n| n as f32) + .ok_or_else(|| Error::new("expected number", 0)) + } +} + +macro_rules! jackson_int_impls { + ($($t:ty),* $(,)?) => { $( + impl ToJson for $t { + fn to_json(&self) -> Value { + Value::Number(*self as f64) + } + } + + impl FromJson for $t { + fn from_json(v: &Value) -> Result { + let n = v + .as_number() + .ok_or_else(|| Error::new("expected number", 0))?; + if n.is_finite() + && n.fract() == 0.0 + && n >= <$t>::MIN as f64 + && n <= <$t>::MAX as f64 + { + Ok(n as $t) + } else { + Err(Error::new("integer out of range", 0)) + } + } + } + )* }; +} + +jackson_int_impls!(i8, i16, i32, i64, u8, u16, u32, u64); + +impl ToJson for Option { + fn to_json(&self) -> Value { + match self { + Some(v) => v.to_json(), + None => Value::Null, + } + } +} + +impl FromJson for Option { + fn from_json(v: &Value) -> Result { + match v { + Value::Null => Ok(None), + _ => T::from_json(v).map(Some), + } + } + + fn missing_field() -> Result { + Ok(None) + } +} + +impl ToJson for Vec { + fn to_json(&self) -> Value { + Value::Array(self.iter().map(T::to_json).collect()) + } +} + +impl FromJson for Vec { + fn from_json(v: &Value) -> Result { + let a = v + .as_array() + .ok_or_else(|| Error::new("expected array", 0))?; + a.iter().map(T::from_json).collect() + } +} + +/// Define a struct together with [`ToJson`] and [`FromJson`] impls. +#[macro_export] +macro_rules! json_struct { + ( + $(#[$meta:meta])* + $vis:vis struct $name:ident { + $( + $(#[$fmeta:meta])* + $fvis:vis $field:ident : $ty:ty + ),* $(,)? + } + ) => { + $(#[$meta])* + $vis struct $name { + $( + $(#[$fmeta])* + $fvis $field: $ty, + )* + } + + impl $crate::ToJson for $name { + fn to_json(&self) -> $crate::Value { + $crate::Value::Object(vec![ + $( + ( + stringify!($field).to_string(), + <$ty as $crate::ToJson>::to_json(&self.$field), + ), + )* + ]) + } + } + + impl $crate::FromJson for $name { + fn from_json( + v: &$crate::Value, + ) -> ::std::result::Result { + let obj = v.as_object().ok_or_else(|| { + $crate::Error::new("expected object", 0) + })?; + $( let mut $field: Option<&$crate::Value> = None; )* + for (k, val) in obj { + match k.as_str() { + $( stringify!($field) => $field = Some(val), )* + _ => {} + } + } + Ok(Self { + $( + $field: match $field { + Some(val) => { + <$ty as $crate::FromJson>::from_json(val)? + } + None => <$ty as $crate::FromJson>::missing_field() + .map_err(|_| $crate::Error::new( + concat!( + "missing field: ", + stringify!($field), + ), + 0, + ))?, + }, + )* + }) + } + } + }; +} + #[cfg(test)] mod tests { use super::*; @@ -819,4 +1024,148 @@ mod tests { let a = Value::Array(vec![Value::Number(f64::NAN)]); assert!(a.stringify().is_err()); } + + json_struct! { + struct SimpleUser { + name: String, + age: i64, + active: bool, + } + } + + #[test] + fn derive_simple_round_trip() { + let u = SimpleUser { + name: "ada".into(), + age: 36, + active: true, + }; + let s = u.to_json().stringify().unwrap(); + let v: Value = s.parse().unwrap(); + let back = SimpleUser::from_json(&v).unwrap(); + assert_eq!(back.name, "ada"); + assert_eq!(back.age, 36); + assert!(back.active); + } + + json_struct! { + struct OptionalFields { + required: String, + maybe: Option, + } + } + + #[test] + fn derive_option_none_serialises_as_null() { + let u = OptionalFields { + required: "x".into(), + maybe: None, + }; + let s = u.to_json().stringify().unwrap(); + assert_eq!(s, r#"{"required":"x","maybe":null}"#); + let back = OptionalFields::from_json(&s.parse().unwrap()).unwrap(); + assert!(back.maybe.is_none()); + } + + #[test] + fn derive_absent_option_is_none() { + let v: Value = r#"{"required":"x"}"#.parse().unwrap(); + let back = OptionalFields::from_json(&v).unwrap(); + assert_eq!(back.required, "x"); + assert!(back.maybe.is_none()); + } + + #[test] + fn derive_option_some_round_trips() { + let u = OptionalFields { + required: "x".into(), + maybe: Some(42), + }; + let s = u.to_json().stringify().unwrap(); + let back = OptionalFields::from_json(&s.parse().unwrap()).unwrap(); + assert_eq!(back.maybe, Some(42)); + } + + json_struct! { + struct Container { + tags: Vec, + counts: Vec, + } + } + + #[test] + fn derive_vec_round_trip() { + let c = Container { + tags: vec!["a".into(), "b".into()], + counts: vec![1, 2, 3], + }; + let s = c.to_json().stringify().unwrap(); + let back = Container::from_json(&s.parse().unwrap()).unwrap(); + assert_eq!(back.tags, vec!["a".to_string(), "b".to_string()]); + assert_eq!(back.counts, vec![1, 2, 3]); + } + + json_struct! { + struct Inner { + value: i32, + } + } + + json_struct! { + struct Outer { + name: String, + inner: Inner, + } + } + + #[test] + fn derive_nested_struct() { + let o = Outer { + name: "n".into(), + inner: Inner { value: 7 }, + }; + let s = o.to_json().stringify().unwrap(); + assert_eq!(s, r#"{"name":"n","inner":{"value":7}}"#); + let back = Outer::from_json(&s.parse().unwrap()).unwrap(); + assert_eq!(back.name, "n"); + assert_eq!(back.inner.value, 7); + } + + #[test] + fn derive_missing_field_errors() { + let v: Value = r#"{"name":"ada","active":true}"#.parse().unwrap(); + let e = SimpleUser::from_json(&v).err().unwrap(); + assert_eq!(e.message(), "missing field: age"); + } + + #[test] + fn derive_wrong_type_errors() { + let v: Value = + r#"{"name":"ada","age":"old","active":true}"#.parse().unwrap(); + assert!(SimpleUser::from_json(&v).is_err()); + } + + #[test] + fn derive_expects_object() { + let v = Value::Array(Vec::new()); + assert!(SimpleUser::from_json(&v).is_err()); + } + + #[test] + fn derive_integer_out_of_range_errors() { + assert!(i32::from_json(&Value::Number(1e20)).is_err()); + } + + #[test] + fn derive_non_integer_errors() { + assert!(i64::from_json(&Value::Number(1.5)).is_err()); + } + + #[test] + fn derive_duplicate_keys_last_wins() { + let v: Value = + r#"{"name":"a","age":1,"active":true,"age":2}"#.parse().unwrap(); + let back = SimpleUser::from_json(&v).unwrap(); + assert_eq!(back.age, 2); + } }