Skip to main content

siderite_validation/
dump.rs

1//! Serialization options (Pydantic `model_dump` equivalents).
2//!
3//! siderite composes with Serde instead of replacing it: a value is first
4//! serialized by Serde, then [`DumpOptions`] are applied. `#[derive(Schema)]`
5//! implements [`Dump`] for models to add computed fields and field/model
6//! serializers and to recurse into nested models, so nested computed fields
7//! also appear.
8
9use serde::{Serialize, Serializer};
10use serde_json::{Map, Value};
11use std::collections::{BTreeMap, HashMap};
12use thiserror::Error;
13
14/// Serialization failure.
15#[derive(Debug, Error)]
16#[error("serialization failed: {0}")]
17pub struct DumpError(#[from] pub serde_json::Error);
18
19/// A tree of field names, for `include` / `exclude`.
20///
21/// A leaf selects the whole field; children select nested fields. Nested
22/// selections apply to every element of lists and maps.
23#[derive(Debug, Clone, Default, PartialEq, Eq)]
24pub struct FieldSet(BTreeMap<String, FieldSet>);
25
26impl FieldSet {
27    /// Empty set.
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    /// Set of top-level fields.
33    pub fn of<I: IntoIterator<Item = S>, S: Into<String>>(fields: I) -> Self {
34        Self(
35            fields
36                .into_iter()
37                .map(|f| (f.into(), Self::new()))
38                .collect(),
39        )
40    }
41
42    /// Add a nested selection `field -> children`.
43    #[must_use]
44    pub fn nested(mut self, field: impl Into<String>, children: FieldSet) -> Self {
45        self.0.insert(field.into(), children);
46        self
47    }
48
49    fn get(&self, field: &str) -> Option<&FieldSet> {
50        self.0.get(field)
51    }
52
53    fn is_leaf(&self) -> bool {
54        self.0.is_empty()
55    }
56}
57
58/// Options controlling serialization output.
59#[derive(Debug, Clone, Default, PartialEq, Eq)]
60pub struct DumpOptions {
61    /// Drop object members whose value is `null`.
62    pub exclude_none: bool,
63    /// Drop fields equal to their declared default (derived models only).
64    pub exclude_defaults: bool,
65    /// Keep only these fields.
66    pub include: Option<FieldSet>,
67    /// Drop these fields.
68    pub exclude: Option<FieldSet>,
69}
70
71impl DumpOptions {
72    /// Default options (plain Serde output).
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    /// Drop `null` members.
78    #[must_use]
79    pub fn exclude_none(mut self) -> Self {
80        self.exclude_none = true;
81        self
82    }
83
84    /// Drop fields equal to their default.
85    #[must_use]
86    pub fn exclude_defaults(mut self) -> Self {
87        self.exclude_defaults = true;
88        self
89    }
90
91    /// Keep only `fields`.
92    #[must_use]
93    pub fn include(mut self, fields: FieldSet) -> Self {
94        self.include = Some(fields);
95        self
96    }
97
98    /// Drop `fields`.
99    #[must_use]
100    pub fn exclude(mut self, fields: FieldSet) -> Self {
101        self.exclude = Some(fields);
102        self
103    }
104
105    /// Options for the member `field` of an object, or `None` if the member
106    /// is excluded entirely.
107    pub fn for_field(&self, field: &str) -> Option<DumpOptions> {
108        let include = match &self.include {
109            None => None,
110            Some(set) => match set.get(field) {
111                None => return None,
112                Some(child) if child.is_leaf() => None,
113                Some(child) => Some(child.clone()),
114            },
115        };
116        let exclude = match &self.exclude {
117            None => None,
118            Some(set) => match set.get(field) {
119                Some(child) if child.is_leaf() => return None,
120                Some(child) => Some(child.clone()),
121                None => None,
122            },
123        };
124        Some(DumpOptions {
125            include,
126            exclude,
127            ..self.clone()
128        })
129    }
130
131    /// Whether a member with this value should be dropped by `exclude_none`.
132    pub fn drops_value(&self, value: &Value) -> bool {
133        self.exclude_none && value.is_null()
134    }
135
136    /// Apply the options to an already-serialized value (used for types
137    /// without derived dumping).
138    pub fn apply(&self, value: Value) -> Value {
139        match value {
140            Value::Object(map) => {
141                let mut out = Map::new();
142                for (key, member) in map {
143                    let Some(child) = self.for_field(&key) else {
144                        continue;
145                    };
146                    let member = child.apply(member);
147                    if !self.drops_value(&member) {
148                        out.insert(key, member);
149                    }
150                }
151                Value::Object(out)
152            }
153            Value::Array(items) => Value::Array(items.into_iter().map(|v| self.apply(v)).collect()),
154            other => other,
155        }
156    }
157}
158
159/// Output serialization with [`DumpOptions`] (Pydantic `model_dump`).
160///
161/// The default serializes with Serde and post-processes the result, so a
162/// hand-written type opts in with `impl Dump for T {}`. `#[derive(Schema)]`
163/// implements it for models: computed fields, field/model serializers,
164/// `exclude_defaults`, and recursion into nested models. Containers delegate
165/// to their elements so nested models keep their computed fields.
166pub trait Dump: Serialize {
167    /// Serialize directly when supported, preserving `dump` semantics.
168    ///
169    /// Args:
170    ///     serializer: Destination serializer.
171    ///     opts: Field selection and exclusion policy.
172    ///
173    /// Returns:
174    ///     Serialized output, or a serialization error.
175    fn serialize_dump<S: Serializer>(
176        &self,
177        serializer: S,
178        opts: &DumpOptions,
179    ) -> Result<S::Ok, S::Error> {
180        self.dump(opts)
181            .map_err(serde::ser::Error::custom)?
182            .serialize(serializer)
183    }
184
185    /// Serialize with `opts` applied.
186    ///
187    /// # Errors
188    /// Serialization failures.
189    fn dump(&self, opts: &DumpOptions) -> Result<Value, DumpError> {
190        Ok(opts.apply(serde_json::to_value(self)?))
191    }
192}
193
194/// Borrowed Serde adapter that preserves a value's `Dump` implementation.
195pub struct DumpSerialize<'a, T: ?Sized>(pub &'a T, pub &'a DumpOptions);
196
197impl<T: Dump + ?Sized> Serialize for DumpSerialize<'_, T> {
198    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
199        self.0.serialize_dump(serializer, self.1)
200    }
201}
202
203macro_rules! plain_dump {
204    ($($t:ty),* $(,)?) => {$(
205        impl Dump for $t {
206            fn serialize_dump<S: Serializer>(
207                &self,
208                serializer: S,
209                _: &DumpOptions,
210            ) -> Result<S::Ok, S::Error> {
211                self.serialize(serializer)
212            }
213        }
214    )*};
215}
216plain_dump!(
217    bool,
218    i8,
219    i16,
220    i32,
221    i64,
222    u8,
223    u16,
224    u32,
225    u64,
226    isize,
227    usize,
228    char,
229    String,
230    str,
231    (),
232);
233
234impl Dump for f32 {}
235impl Dump for f64 {}
236impl Dump for i128 {}
237impl Dump for u128 {}
238impl Dump for Value {}
239impl Dump for Map<String, Value> {}
240
241impl<T: Dump + ?Sized> Dump for &T {
242    fn serialize_dump<S: Serializer>(
243        &self,
244        serializer: S,
245        opts: &DumpOptions,
246    ) -> Result<S::Ok, S::Error> {
247        (**self).serialize_dump(serializer, opts)
248    }
249
250    fn dump(&self, opts: &DumpOptions) -> Result<Value, DumpError> {
251        (**self).dump(opts)
252    }
253}
254
255impl<T: Dump + ?Sized> Dump for Box<T> {
256    fn serialize_dump<S: Serializer>(
257        &self,
258        serializer: S,
259        opts: &DumpOptions,
260    ) -> Result<S::Ok, S::Error> {
261        (**self).serialize_dump(serializer, opts)
262    }
263
264    fn dump(&self, opts: &DumpOptions) -> Result<Value, DumpError> {
265        (**self).dump(opts)
266    }
267}
268
269impl<T: Dump> Dump for Option<T> {
270    fn serialize_dump<S: Serializer>(
271        &self,
272        serializer: S,
273        opts: &DumpOptions,
274    ) -> Result<S::Ok, S::Error> {
275        match self {
276            Some(value) => value.serialize_dump(serializer, opts),
277            None => serializer.serialize_none(),
278        }
279    }
280
281    fn dump(&self, opts: &DumpOptions) -> Result<Value, DumpError> {
282        self.as_ref()
283            .map_or(Ok(Value::Null), |inner| inner.dump(opts))
284    }
285}
286
287impl<T: Dump> Dump for [T] {
288    fn serialize_dump<S: Serializer>(
289        &self,
290        serializer: S,
291        opts: &DumpOptions,
292    ) -> Result<S::Ok, S::Error> {
293        use serde::ser::SerializeSeq;
294        let mut sequence = serializer.serialize_seq(Some(self.len()))?;
295        for value in self {
296            sequence.serialize_element(&DumpSerialize(value, opts))?;
297        }
298        sequence.end()
299    }
300
301    fn dump(&self, opts: &DumpOptions) -> Result<Value, DumpError> {
302        self.iter()
303            .map(|item| item.dump(opts))
304            .collect::<Result<_, _>>()
305            .map(Value::Array)
306    }
307}
308
309impl<T: Dump> Dump for Vec<T> {
310    fn serialize_dump<S: Serializer>(
311        &self,
312        serializer: S,
313        opts: &DumpOptions,
314    ) -> Result<S::Ok, S::Error> {
315        self.as_slice().serialize_dump(serializer, opts)
316    }
317
318    fn dump(&self, opts: &DumpOptions) -> Result<Value, DumpError> {
319        self.as_slice().dump(opts)
320    }
321}
322
323fn dump_map<'a, T: Dump + 'a>(
324    entries: impl Iterator<Item = (&'a String, &'a T)>,
325    opts: &DumpOptions,
326) -> Result<Value, DumpError> {
327    let mut out = Map::new();
328    for (key, value) in entries {
329        let value = value.dump(opts)?;
330        if !opts.drops_value(&value) {
331            out.insert(key.clone(), value);
332        }
333    }
334    Ok(Value::Object(out))
335}
336
337impl<T: Dump> Dump for BTreeMap<String, T> {
338    fn dump(&self, opts: &DumpOptions) -> Result<Value, DumpError> {
339        dump_map(self.iter(), opts)
340    }
341}
342
343impl<T: Dump, S: std::hash::BuildHasher> Dump for HashMap<String, T, S> {
344    fn dump(&self, opts: &DumpOptions) -> Result<Value, DumpError> {
345        dump_map(self.iter(), opts)
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use serde_json::json;
353
354    #[test]
355    fn exclude_none_and_nested_exclude() {
356        let opts = DumpOptions::new()
357            .exclude_none()
358            .exclude(FieldSet::new().nested("user", FieldSet::of(["password"])));
359        let out = opts.apply(json!({"a": null, "user": {"name": "x", "password": "p"}, "b": 1}));
360        assert_eq!(out, json!({"user": {"name": "x"}, "b": 1}));
361    }
362
363    #[test]
364    fn include_applies_through_lists() {
365        let opts =
366            DumpOptions::new().include(FieldSet::new().nested("items", FieldSet::of(["id"])));
367        let out = opts.apply(json!({"items": [{"id": 1, "x": 2}], "other": 3}));
368        assert_eq!(out, json!({"items": [{"id": 1}]}));
369    }
370}