Skip to main content

rusdantic_core/
dump.rs

1//! Advanced serialization with options (model_dump / model_dump_json equivalent).
2//!
3//! Provides `DumpOptions` for controlling serialization output: include/exclude
4//! fields, use aliases, skip unset/default/none values.
5
6use serde::Serialize;
7use serde_json::Value;
8use std::collections::HashSet;
9
10/// Options for controlling serialization output.
11///
12/// Mirrors Pydantic's `model_dump()` parameters.
13///
14/// # Example
15///
16/// ```rust
17/// use rusdantic_core::dump::DumpOptions;
18///
19/// let opts = DumpOptions::new()
20///     .exclude(&["password", "secret"])
21///     .exclude_none(true);
22/// ```
23#[derive(Debug, Clone, Default)]
24pub struct DumpOptions {
25    /// Only include these fields in the output (whitelist).
26    /// If empty, all fields are included.
27    pub include_fields: HashSet<String>,
28    /// Exclude these fields from the output (blacklist).
29    pub exclude_fields: HashSet<String>,
30    /// If true, skip fields with `None` values.
31    pub exclude_none: bool,
32    // NOTE: The following options require derive-macro-generated metadata
33    // (default values, alias mappings, fields_set tracking) and are planned
34    // for a future release. They are intentionally NOT public fields to
35    // prevent users from setting them with no effect.
36    //
37    // Planned: exclude_defaults, by_alias, exclude_unset
38    /// Indentation for JSON output (None = compact).
39    pub indent: Option<usize>,
40    /// If true, apply include/exclude/exclude_none recursively to nested objects.
41    /// Default: false (only filter top-level fields, matching Pydantic behavior).
42    pub recursive: bool,
43}
44
45impl DumpOptions {
46    /// Create a new `DumpOptions` with default settings (include all, compact).
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Only include the specified fields in the output.
52    pub fn include(mut self, fields: &[&str]) -> Self {
53        self.include_fields = fields.iter().map(|s| s.to_string()).collect();
54        self
55    }
56
57    /// Exclude the specified fields from the output.
58    pub fn exclude(mut self, fields: &[&str]) -> Self {
59        self.exclude_fields = fields.iter().map(|s| s.to_string()).collect();
60        self
61    }
62
63    /// If true, skip fields with `None` / `null` values.
64    pub fn exclude_none(mut self, yes: bool) -> Self {
65        self.exclude_none = yes;
66        self
67    }
68
69
70    /// Set JSON indentation (None = compact).
71    pub fn indent(mut self, spaces: usize) -> Self {
72        self.indent = Some(spaces);
73        self
74    }
75
76    /// Apply options to a serialized JSON Value, filtering fields.
77    ///
78    /// By default, only top-level fields are filtered (matching Pydantic behavior).
79    /// Set `recursive(true)` to also filter nested objects.
80    pub fn filter_value(&self, value: &mut Value) {
81        if self.recursive {
82            self.filter_value_recursive(value, 0);
83        } else {
84            self.filter_value_top_level(value);
85        }
86    }
87
88    /// Filter only top-level fields (default behavior).
89    fn filter_value_top_level(&self, value: &mut Value) {
90        if let Value::Object(ref mut map) = value {
91            let keys_to_remove: Vec<String> = map
92                .keys()
93                .filter(|key| {
94                    if !self.include_fields.is_empty() && !self.include_fields.contains(*key) {
95                        return true;
96                    }
97                    if self.exclude_fields.contains(*key) {
98                        return true;
99                    }
100                    if self.exclude_none {
101                        if let Some(val) = map.get(*key) {
102                            if val.is_null() {
103                                return true;
104                            }
105                        }
106                    }
107                    false
108                })
109                .cloned()
110                .collect();
111
112            for key in keys_to_remove {
113                map.remove(&key);
114            }
115        }
116    }
117
118    /// Set whether filtering applies recursively to nested objects.
119    pub fn recursive(mut self, yes: bool) -> Self {
120        self.recursive = yes;
121        self
122    }
123
124    /// Internal recursive filter with depth limit to prevent stack overflow.
125    fn filter_value_recursive(&self, value: &mut Value, depth: usize) {
126        // Depth limit to prevent stack overflow on pathological inputs
127        const MAX_DEPTH: usize = 128;
128        if depth > MAX_DEPTH {
129            return;
130        }
131
132        if let Value::Object(ref mut map) = value {
133            // Collect keys to remove
134            let keys_to_remove: Vec<String> = map
135                .keys()
136                .filter(|key| {
137                    // Check include list (if non-empty, only whitelisted keys survive)
138                    if !self.include_fields.is_empty() && !self.include_fields.contains(*key) {
139                        return true;
140                    }
141                    // Check exclude list
142                    if self.exclude_fields.contains(*key) {
143                        return true;
144                    }
145                    // Check exclude_none
146                    if self.exclude_none {
147                        if let Some(val) = map.get(*key) {
148                            if val.is_null() {
149                                return true;
150                            }
151                        }
152                    }
153                    false
154                })
155                .cloned()
156                .collect();
157
158            for key in keys_to_remove {
159                map.remove(&key);
160            }
161
162            // Recursively filter nested objects and arrays
163            for (_, v) in map.iter_mut() {
164                self.filter_value_recursive(v, depth + 1);
165            }
166        } else if let Value::Array(ref mut arr) = value {
167            // Apply filtering to objects within arrays
168            for item in arr.iter_mut() {
169                self.filter_value_recursive(item, depth + 1);
170            }
171        }
172    }
173}
174
175/// Trait for types that support advanced serialization with options.
176///
177/// Automatically implemented for any type that implements `serde::Serialize`.
178pub trait Dump: Serialize {
179    /// Serialize to a `serde_json::Value` with default options.
180    fn dump(&self) -> Result<Value, serde_json::Error> {
181        serde_json::to_value(self)
182    }
183
184    /// Serialize to a `serde_json::Value` with custom options.
185    fn dump_with(&self, options: &DumpOptions) -> Result<Value, serde_json::Error> {
186        let mut value = serde_json::to_value(self)?;
187        options.filter_value(&mut value);
188        Ok(value)
189    }
190
191    /// Serialize to a JSON string with default options.
192    fn dump_json(&self) -> Result<String, serde_json::Error> {
193        serde_json::to_string(self)
194    }
195
196    /// Serialize to a JSON string with custom options.
197    fn dump_json_with(&self, options: &DumpOptions) -> Result<String, serde_json::Error> {
198        let mut value = serde_json::to_value(self)?;
199        options.filter_value(&mut value);
200        if let Some(indent) = options.indent {
201            // Pretty print with custom indent
202            let buf = Vec::new();
203            let indent_bytes = " ".repeat(indent).into_bytes();
204            let formatter = serde_json::ser::PrettyFormatter::with_indent(&indent_bytes);
205            let mut ser = serde_json::Serializer::with_formatter(buf, formatter);
206            serde::Serialize::serialize(&value, &mut ser)
207                .map_err(serde_json::Error::from)?;
208            // SAFETY: serde_json always produces valid UTF-8
209            Ok(String::from_utf8(ser.into_inner()).unwrap())
210        } else {
211            serde_json::to_string(&value)
212        }
213    }
214}
215
216// Blanket implementation: any Serialize type gets Dump for free
217impl<T: Serialize> Dump for T {}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use serde_json::json;
223
224    #[test]
225    fn test_dump_options_exclude() {
226        let opts = DumpOptions::new().exclude(&["password", "secret"]);
227        let mut value = json!({"name": "alice", "password": "hidden", "secret": "key"});
228        opts.filter_value(&mut value);
229        assert!(value.get("name").is_some());
230        assert!(value.get("password").is_none());
231        assert!(value.get("secret").is_none());
232    }
233
234    #[test]
235    fn test_dump_options_include() {
236        let opts = DumpOptions::new().include(&["name", "email"]);
237        let mut value = json!({"name": "alice", "email": "a@b.com", "age": 30});
238        opts.filter_value(&mut value);
239        assert!(value.get("name").is_some());
240        assert!(value.get("email").is_some());
241        assert!(value.get("age").is_none());
242    }
243
244    #[test]
245    fn test_dump_options_exclude_none() {
246        let opts = DumpOptions::new().exclude_none(true);
247        let mut value = json!({"name": "alice", "bio": null, "age": 30});
248        opts.filter_value(&mut value);
249        assert!(value.get("name").is_some());
250        assert!(value.get("bio").is_none());
251        assert!(value.get("age").is_some());
252    }
253
254    #[test]
255    fn test_dump_options_combined() {
256        let opts = DumpOptions::new()
257            .exclude(&["password"])
258            .exclude_none(true);
259        let mut value = json!({"name": "alice", "password": "x", "bio": null});
260        opts.filter_value(&mut value);
261        assert!(value.get("name").is_some());
262        assert!(value.get("password").is_none());
263        assert!(value.get("bio").is_none());
264    }
265
266    #[test]
267    fn test_dump_trait_on_serialize() {
268        #[derive(serde::Serialize)]
269        struct User {
270            name: String,
271            age: u32,
272        }
273        let user = User {
274            name: "alice".to_string(),
275            age: 30,
276        };
277        let value = user.dump().unwrap();
278        assert_eq!(value["name"], "alice");
279        assert_eq!(value["age"], 30);
280    }
281
282    #[test]
283    fn test_dump_with_exclude() {
284        #[derive(serde::Serialize)]
285        struct User {
286            name: String,
287            password: String,
288        }
289        let user = User {
290            name: "alice".to_string(),
291            password: "secret".to_string(),
292        };
293        let opts = DumpOptions::new().exclude(&["password"]);
294        let value = user.dump_with(&opts).unwrap();
295        assert!(value.get("name").is_some());
296        assert!(value.get("password").is_none());
297    }
298
299    #[test]
300    fn test_dump_json_compact() {
301        #[derive(serde::Serialize)]
302        struct Item {
303            name: String,
304        }
305        let item = Item {
306            name: "test".to_string(),
307        };
308        let json = item.dump_json().unwrap();
309        assert_eq!(json, r#"{"name":"test"}"#);
310    }
311
312    #[test]
313    fn test_dump_json_with_indent() {
314        #[derive(serde::Serialize)]
315        struct Item {
316            name: String,
317        }
318        let item = Item {
319            name: "test".to_string(),
320        };
321        let opts = DumpOptions::new().indent(2);
322        let json = item.dump_json_with(&opts).unwrap();
323        assert!(json.contains("\n"));
324        assert!(json.contains("  "));
325    }
326}