superdoc-sdk 0.1.0-next.beta.31

Typed Rust client for the SuperDoc document runtime
Documentation
//! JSON value semantics shared by generated types. Missing fields and explicit
//! null values have different meanings for document updates.

use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use std::collections::BTreeMap;

/// An optional field. `Missing` omits its object key; `Present` preserves its
/// value, including null when the field's value type permits null.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum Presence<T> {
    #[default]
    Missing,
    Present(T),
}

impl<T> Presence<T> {
    pub fn is_missing(&self) -> bool {
        matches!(self, Self::Missing)
    }

    pub fn as_ref(&self) -> Presence<&T> {
        match self {
            Self::Missing => Presence::Missing,
            Self::Present(value) => Presence::Present(value),
        }
    }
}

impl<T: Serialize> Serialize for Presence<T> {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        match self {
            Self::Present(value) => value.serialize(serializer),
            Self::Missing => Err(serde::ser::Error::custom(
                "a missing field must be omitted by its containing object",
            )),
        }
    }
}

impl<'de, T: Deserialize<'de>> Deserialize<'de> for Presence<T> {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        T::deserialize(deserializer).map(Self::Present)
    }
}

/// A present JSON value that may explicitly be null. Required fields also use
/// the generated required-field deserializer so absence cannot become null.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Nullable<T> {
    Null,
    Value(T),
}

impl<T: Serialize> Serialize for Nullable<T> {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        match self {
            Self::Null => serializer.serialize_none(),
            Self::Value(value) => value.serialize(serializer),
        }
    }
}

impl<'de, T: DeserializeOwned> Deserialize<'de> for Nullable<T> {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let value = Value::deserialize(deserializer)?;
        if value.is_null() {
            Ok(Self::Null)
        } else {
            serde_json::from_value(value)
                .map(Self::Value)
                .map_err(serde::de::Error::custom)
        }
    }
}

/// Deserialize a field that must be present, even when `T` itself accepts null.
pub fn required<'de, T: Deserialize<'de>, D: Deserializer<'de>>(
    deserializer: D,
) -> Result<T, D::Error> {
    T::deserialize(deserializer)
}

/// Shape matching is deliberately narrower than host-side JSON Schema
/// validation: ranges, patterns, conditional presence and oneOf exclusivity
/// remain enforced by the SuperDoc host.
pub trait Shape {
    fn matches(value: &Value) -> bool;
}

impl Shape for Value {
    fn matches(_: &Value) -> bool {
        true
    }
}
impl Shape for String {
    fn matches(value: &Value) -> bool {
        value.is_string()
    }
}
impl Shape for bool {
    fn matches(value: &Value) -> bool {
        value.is_boolean()
    }
}
impl<T: Shape> Shape for Vec<T> {
    fn matches(value: &Value) -> bool {
        value
            .as_array()
            .is_some_and(|items| items.iter().all(T::matches))
    }
}
impl<T: Shape> Shape for BTreeMap<String, T> {
    fn matches(value: &Value) -> bool {
        value
            .as_object()
            .is_some_and(|items| items.values().all(T::matches))
    }
}
impl<T: Shape> Shape for Box<T> {
    fn matches(value: &Value) -> bool {
        T::matches(value)
    }
}
impl<T: Shape> Shape for Nullable<T> {
    fn matches(value: &Value) -> bool {
        value.is_null() || T::matches(value)
    }
}

/// JSON null without an optional-field interpretation.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Null;
impl Serialize for Null {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_none()
    }
}
impl<'de> Deserialize<'de> for Null {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        <()>::deserialize(deserializer).map(|()| Self)
    }
}
impl Shape for Null {
    fn matches(value: &Value) -> bool {
        value.is_null()
    }
}

/// An integer exactly representable by the JavaScript host. Wider integers
/// must not be silently rounded when sent through JSON.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Integer(i64);
impl Integer {
    pub const MAX: i64 = 9_007_199_254_740_991;

    pub fn new(value: i64) -> Result<Self, &'static str> {
        if (-Self::MAX..=Self::MAX).contains(&value) {
            Ok(Self(value))
        } else {
            Err("integer exceeds the SuperDoc host's exact JSON integer range")
        }
    }

    pub fn get(self) -> i64 {
        self.0
    }
}
impl TryFrom<i64> for Integer {
    type Error = &'static str;
    fn try_from(value: i64) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}
impl Serialize for Integer {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_i64(self.0)
    }
}
impl<'de> Deserialize<'de> for Integer {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let value = Value::deserialize(deserializer)?;
        let number = value
            .as_f64()
            .filter(|n| n.is_finite() && n.fract() == 0.0 && n.abs() <= Self::MAX as f64);
        number.map(|n| Self(n as i64)).ok_or_else(|| {
            serde::de::Error::custom("expected an exactly representable JSON integer")
        })
    }
}
impl Shape for Integer {
    fn matches(value: &Value) -> bool {
        serde_json::from_value::<Self>(value.clone()).is_ok()
    }
}

/// A finite JSON number. Non-finite floats cannot silently serialize as null.
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct Number(f64);
impl Number {
    pub fn new(value: f64) -> Result<Self, &'static str> {
        if value.is_finite() {
            Ok(Self(value))
        } else {
            Err("JSON numbers must be finite")
        }
    }
    pub fn get(self) -> f64 {
        self.0
    }
}
impl Serialize for Number {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_f64(self.0)
    }
}
impl<'de> Deserialize<'de> for Number {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        Self::new(f64::deserialize(deserializer)?).map_err(serde::de::Error::custom)
    }
}
impl Shape for Number {
    fn matches(value: &Value) -> bool {
        value.as_f64().is_some_and(f64::is_finite)
    }
}

/// The impossible value described by a false schema.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Never {}
impl Serialize for Never {
    fn serialize<S: Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
        match *self {}
    }
}
impl<'de> Deserialize<'de> for Never {
    fn deserialize<D: Deserializer<'de>>(_: D) -> Result<Self, D::Error> {
        Err(serde::de::Error::custom("the schema accepts no values"))
    }
}
impl Shape for Never {
    fn matches(_: &Value) -> bool {
        false
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::{from_value, json, to_value};

    #[derive(Debug, Deserialize, Serialize)]
    struct Fields {
        #[serde(deserialize_with = "required")]
        required_nullable: Nullable<String>,
        #[serde(default, skip_serializing_if = "Presence::is_missing")]
        optional_nullable: Presence<Nullable<String>>,
        #[serde(default, skip_serializing_if = "Presence::is_missing")]
        optional_text: Presence<String>,
        #[serde(deserialize_with = "required")]
        json: Value,
    }

    #[test]
    fn preserves_missing_null_and_value() {
        for value in [
            json!({"required_nullable":null,"json":null}),
            json!({"required_nullable":"é 😀","optional_nullable":null,"json":{}}),
            json!({"required_nullable":null,"optional_nullable":"","optional_text":"","json":false}),
        ] {
            let decoded: Fields = from_value(value.clone()).unwrap();
            assert_eq!(to_value(decoded).unwrap(), value);
        }
    }

    #[test]
    fn required_null_fields_reject_absence() {
        assert!(from_value::<Fields>(json!({"json":null})).is_err());
        assert!(from_value::<Fields>(json!({"required_nullable":null})).is_err());
    }

    #[test]
    fn optional_nonnullable_rejects_null() {
        assert!(from_value::<Fields>(
            json!({"required_nullable":null,"optional_text":null,"json":null})
        )
        .is_err());
    }

    #[test]
    fn exact_integer_boundaries_and_fraction_rejection() {
        for value in [-Integer::MAX, -1, 0, 1, Integer::MAX] {
            assert_eq!(Integer::new(value).unwrap().get(), value);
            assert_eq!(from_value::<Integer>(json!(value)).unwrap().get(), value);
        }
        for value in [
            json!(Integer::MAX + 1),
            json!(1.5),
            json!(true),
            Value::Null,
        ] {
            assert!(from_value::<Integer>(value).is_err());
        }
        assert!(Integer::new(i64::MIN).is_err());
        assert_eq!(from_value::<Integer>(json!(1.0)).unwrap().get(), 1);
    }

    #[test]
    fn nonfinite_numbers_are_rejected() {
        assert!(Number::new(f64::NAN).is_err());
        assert!(Number::new(f64::INFINITY).is_err());
        assert!(Number::new(f64::NEG_INFINITY).is_err());
        assert_eq!(to_value(Number::new(0.5).unwrap()).unwrap(), json!(0.5));
    }

    #[test]
    fn missing_values_cannot_leak_into_arrays_as_null() {
        assert!(to_value(vec![Presence::<String>::Missing]).is_err());
    }
}