Skip to main content

superdoc_sdk/
wire.rs

1//! JSON value semantics shared by generated types. Missing fields and explicit
2//! null values have different meanings for document updates.
3
4use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize, Serializer};
5use serde_json::Value;
6use std::collections::BTreeMap;
7
8/// An optional field. `Missing` omits its object key; `Present` preserves its
9/// value, including null when the field's value type permits null.
10#[derive(Clone, Debug, Default, PartialEq, Eq)]
11pub enum Presence<T> {
12    #[default]
13    Missing,
14    Present(T),
15}
16
17impl<T> Presence<T> {
18    pub fn is_missing(&self) -> bool {
19        matches!(self, Self::Missing)
20    }
21
22    pub fn as_ref(&self) -> Presence<&T> {
23        match self {
24            Self::Missing => Presence::Missing,
25            Self::Present(value) => Presence::Present(value),
26        }
27    }
28}
29
30impl<T: Serialize> Serialize for Presence<T> {
31    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
32        match self {
33            Self::Present(value) => value.serialize(serializer),
34            Self::Missing => Err(serde::ser::Error::custom(
35                "a missing field must be omitted by its containing object",
36            )),
37        }
38    }
39}
40
41impl<'de, T: Deserialize<'de>> Deserialize<'de> for Presence<T> {
42    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
43        T::deserialize(deserializer).map(Self::Present)
44    }
45}
46
47/// A present JSON value that may explicitly be null. Required fields also use
48/// the generated required-field deserializer so absence cannot become null.
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub enum Nullable<T> {
51    Null,
52    Value(T),
53}
54
55impl<T: Serialize> Serialize for Nullable<T> {
56    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
57        match self {
58            Self::Null => serializer.serialize_none(),
59            Self::Value(value) => value.serialize(serializer),
60        }
61    }
62}
63
64impl<'de, T: DeserializeOwned> Deserialize<'de> for Nullable<T> {
65    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
66        let value = Value::deserialize(deserializer)?;
67        if value.is_null() {
68            Ok(Self::Null)
69        } else {
70            serde_json::from_value(value)
71                .map(Self::Value)
72                .map_err(serde::de::Error::custom)
73        }
74    }
75}
76
77/// Deserialize a field that must be present, even when `T` itself accepts null.
78pub fn required<'de, T: Deserialize<'de>, D: Deserializer<'de>>(
79    deserializer: D,
80) -> Result<T, D::Error> {
81    T::deserialize(deserializer)
82}
83
84/// Shape matching is deliberately narrower than host-side JSON Schema
85/// validation: ranges, patterns, conditional presence and oneOf exclusivity
86/// remain enforced by the SuperDoc host.
87pub trait Shape {
88    fn matches(value: &Value) -> bool;
89}
90
91impl Shape for Value {
92    fn matches(_: &Value) -> bool {
93        true
94    }
95}
96impl Shape for String {
97    fn matches(value: &Value) -> bool {
98        value.is_string()
99    }
100}
101impl Shape for bool {
102    fn matches(value: &Value) -> bool {
103        value.is_boolean()
104    }
105}
106impl<T: Shape> Shape for Vec<T> {
107    fn matches(value: &Value) -> bool {
108        value
109            .as_array()
110            .is_some_and(|items| items.iter().all(T::matches))
111    }
112}
113impl<T: Shape> Shape for BTreeMap<String, T> {
114    fn matches(value: &Value) -> bool {
115        value
116            .as_object()
117            .is_some_and(|items| items.values().all(T::matches))
118    }
119}
120impl<T: Shape> Shape for Box<T> {
121    fn matches(value: &Value) -> bool {
122        T::matches(value)
123    }
124}
125impl<T: Shape> Shape for Nullable<T> {
126    fn matches(value: &Value) -> bool {
127        value.is_null() || T::matches(value)
128    }
129}
130
131/// JSON null without an optional-field interpretation.
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
133pub struct Null;
134impl Serialize for Null {
135    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
136        serializer.serialize_none()
137    }
138}
139impl<'de> Deserialize<'de> for Null {
140    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
141        <()>::deserialize(deserializer).map(|()| Self)
142    }
143}
144impl Shape for Null {
145    fn matches(value: &Value) -> bool {
146        value.is_null()
147    }
148}
149
150/// An integer exactly representable by the JavaScript host. Wider integers
151/// must not be silently rounded when sent through JSON.
152#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
153pub struct Integer(i64);
154impl Integer {
155    pub const MAX: i64 = 9_007_199_254_740_991;
156
157    pub fn new(value: i64) -> Result<Self, &'static str> {
158        if (-Self::MAX..=Self::MAX).contains(&value) {
159            Ok(Self(value))
160        } else {
161            Err("integer exceeds the SuperDoc host's exact JSON integer range")
162        }
163    }
164
165    pub fn get(self) -> i64 {
166        self.0
167    }
168}
169impl TryFrom<i64> for Integer {
170    type Error = &'static str;
171    fn try_from(value: i64) -> Result<Self, Self::Error> {
172        Self::new(value)
173    }
174}
175impl Serialize for Integer {
176    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
177        serializer.serialize_i64(self.0)
178    }
179}
180impl<'de> Deserialize<'de> for Integer {
181    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
182        let value = Value::deserialize(deserializer)?;
183        let number = value
184            .as_f64()
185            .filter(|n| n.is_finite() && n.fract() == 0.0 && n.abs() <= Self::MAX as f64);
186        number.map(|n| Self(n as i64)).ok_or_else(|| {
187            serde::de::Error::custom("expected an exactly representable JSON integer")
188        })
189    }
190}
191impl Shape for Integer {
192    fn matches(value: &Value) -> bool {
193        serde_json::from_value::<Self>(value.clone()).is_ok()
194    }
195}
196
197/// A finite JSON number. Non-finite floats cannot silently serialize as null.
198#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
199pub struct Number(f64);
200impl Number {
201    pub fn new(value: f64) -> Result<Self, &'static str> {
202        if value.is_finite() {
203            Ok(Self(value))
204        } else {
205            Err("JSON numbers must be finite")
206        }
207    }
208    pub fn get(self) -> f64 {
209        self.0
210    }
211}
212impl Serialize for Number {
213    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
214        serializer.serialize_f64(self.0)
215    }
216}
217impl<'de> Deserialize<'de> for Number {
218    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
219        Self::new(f64::deserialize(deserializer)?).map_err(serde::de::Error::custom)
220    }
221}
222impl Shape for Number {
223    fn matches(value: &Value) -> bool {
224        value.as_f64().is_some_and(f64::is_finite)
225    }
226}
227
228/// The impossible value described by a false schema.
229#[derive(Clone, Debug, PartialEq, Eq)]
230pub enum Never {}
231impl Serialize for Never {
232    fn serialize<S: Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
233        match *self {}
234    }
235}
236impl<'de> Deserialize<'de> for Never {
237    fn deserialize<D: Deserializer<'de>>(_: D) -> Result<Self, D::Error> {
238        Err(serde::de::Error::custom("the schema accepts no values"))
239    }
240}
241impl Shape for Never {
242    fn matches(_: &Value) -> bool {
243        false
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use serde_json::{from_value, json, to_value};
251
252    #[derive(Debug, Deserialize, Serialize)]
253    struct Fields {
254        #[serde(deserialize_with = "required")]
255        required_nullable: Nullable<String>,
256        #[serde(default, skip_serializing_if = "Presence::is_missing")]
257        optional_nullable: Presence<Nullable<String>>,
258        #[serde(default, skip_serializing_if = "Presence::is_missing")]
259        optional_text: Presence<String>,
260        #[serde(deserialize_with = "required")]
261        json: Value,
262    }
263
264    #[test]
265    fn preserves_missing_null_and_value() {
266        for value in [
267            json!({"required_nullable":null,"json":null}),
268            json!({"required_nullable":"é 😀","optional_nullable":null,"json":{}}),
269            json!({"required_nullable":null,"optional_nullable":"","optional_text":"","json":false}),
270        ] {
271            let decoded: Fields = from_value(value.clone()).unwrap();
272            assert_eq!(to_value(decoded).unwrap(), value);
273        }
274    }
275
276    #[test]
277    fn required_null_fields_reject_absence() {
278        assert!(from_value::<Fields>(json!({"json":null})).is_err());
279        assert!(from_value::<Fields>(json!({"required_nullable":null})).is_err());
280    }
281
282    #[test]
283    fn optional_nonnullable_rejects_null() {
284        assert!(from_value::<Fields>(
285            json!({"required_nullable":null,"optional_text":null,"json":null})
286        )
287        .is_err());
288    }
289
290    #[test]
291    fn exact_integer_boundaries_and_fraction_rejection() {
292        for value in [-Integer::MAX, -1, 0, 1, Integer::MAX] {
293            assert_eq!(Integer::new(value).unwrap().get(), value);
294            assert_eq!(from_value::<Integer>(json!(value)).unwrap().get(), value);
295        }
296        for value in [
297            json!(Integer::MAX + 1),
298            json!(1.5),
299            json!(true),
300            Value::Null,
301        ] {
302            assert!(from_value::<Integer>(value).is_err());
303        }
304        assert!(Integer::new(i64::MIN).is_err());
305        assert_eq!(from_value::<Integer>(json!(1.0)).unwrap().get(), 1);
306    }
307
308    #[test]
309    fn nonfinite_numbers_are_rejected() {
310        assert!(Number::new(f64::NAN).is_err());
311        assert!(Number::new(f64::INFINITY).is_err());
312        assert!(Number::new(f64::NEG_INFINITY).is_err());
313        assert_eq!(to_value(Number::new(0.5).unwrap()).unwrap(), json!(0.5));
314    }
315
316    #[test]
317    fn missing_values_cannot_leak_into_arrays_as_null() {
318        assert!(to_value(vec![Presence::<String>::Missing]).is_err());
319    }
320}