Skip to main content

rustrails_model/
serialization.rs

1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3use serde_json::{Map, Value};
4
5use crate::Attributes;
6
7pub type SerializationMap = IndexMap<String, Value>;
8
9/// Options controlling model serialization.
10#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
11pub struct SerializationOptions {
12    /// Only include these attributes.
13    pub only: Option<Vec<String>>,
14    /// Exclude these attributes.
15    pub except: Option<Vec<String>>,
16    /// Include computed values by name when they can be read dynamically.
17    pub methods: Option<Vec<String>>,
18    /// Include serialized associations in the requested order.
19    pub include: Option<Vec<SerializationInclude>>,
20    /// Wrap the serialized payload under the given root key.
21    pub root: Option<String>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub enum SerializationInclude {
26    Association(String),
27    Nested {
28        association: String,
29        options: SerializationOptions,
30    },
31}
32
33impl SerializationInclude {
34    pub fn named(name: impl Into<String>) -> Self {
35        Self::Association(name.into())
36    }
37
38    pub fn with_options(name: impl Into<String>, options: SerializationOptions) -> Self {
39        Self::Nested {
40            association: name.into(),
41            options,
42        }
43    }
44
45    fn association(&self) -> &str {
46        match self {
47            Self::Association(association) => association,
48            Self::Nested { association, .. } => association,
49        }
50    }
51
52    fn options(&self) -> Option<SerializationOptions> {
53        match self {
54            Self::Association(_) => None,
55            Self::Nested { options, .. } => Some(options.clone()),
56        }
57    }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61struct SerializationAssociationRequest {
62    association: String,
63    options: SerializationOptions,
64}
65
66const ASSOCIATION_REQUEST_PREFIX: &str = "__rustrails_association__=";
67
68pub fn association_serialization_key(
69    association: impl Into<String>,
70    options: Option<SerializationOptions>,
71) -> String {
72    let request = SerializationAssociationRequest {
73        association: association.into(),
74        options: options.unwrap_or_default(),
75    };
76
77    format!(
78        "{ASSOCIATION_REQUEST_PREFIX}{}",
79        serde_json::to_string(&request).expect("association request should serialize")
80    )
81}
82
83pub fn parse_association_serialization_key(name: &str) -> Option<(String, SerializationOptions)> {
84    let payload = name.strip_prefix(ASSOCIATION_REQUEST_PREFIX)?;
85    let request = serde_json::from_str::<SerializationAssociationRequest>(payload).ok()?;
86    Some((request.association, request.options))
87}
88
89impl SerializationOptions {
90    /// Creates an empty set of serialization options.
91    pub fn new() -> Self {
92        Self::default()
93    }
94
95    /// Restricts serialization to the provided attributes.
96    pub fn only(mut self, attrs: Vec<String>) -> Self {
97        self.only = Some(attrs);
98        self
99    }
100
101    /// Excludes the provided attributes.
102    pub fn except(mut self, attrs: Vec<String>) -> Self {
103        self.except = Some(attrs);
104        self
105    }
106
107    /// Appends computed values by name when available.
108    pub fn methods(mut self, methods: Vec<String>) -> Self {
109        self.methods = Some(methods);
110        self
111    }
112
113    /// Appends associations in the requested order.
114    pub fn include(mut self, include: Vec<SerializationInclude>) -> Self {
115        self.include = Some(include);
116        self
117    }
118
119    /// Wraps the payload under the provided root key.
120    pub fn root(mut self, root: String) -> Self {
121        self.root = Some(root);
122        self
123    }
124}
125
126/// Default hash and JSON serialization for attribute-backed models.
127pub trait Serialization: Attributes {
128    /// Returns a serializable attribute map.
129    fn serializable_hash(&self, options: Option<SerializationOptions>) -> SerializationMap {
130        let options = options.unwrap_or_default();
131        let attribute_names = filter_attribute_names(Self::attribute_names(), &options);
132        let mut serialized = SerializationMap::with_capacity(
133            attribute_names.len()
134                + options.methods.as_ref().map_or(0, Vec::len)
135                + options.include.as_ref().map_or(0, Vec::len),
136        );
137
138        for attribute_name in attribute_names {
139            let value = self.read_attribute(&attribute_name).unwrap_or(Value::Null);
140            serialized.insert(attribute_name, value);
141        }
142
143        if let Some(methods) = options.methods.as_ref() {
144            for method_name in methods {
145                if let Some(value) = self.read_attribute(method_name) {
146                    serialized.insert(method_name.clone(), value);
147                }
148            }
149        }
150
151        if let Some(includes) = options.include.as_ref() {
152            for include in includes {
153                let request =
154                    association_serialization_key(include.association(), include.options());
155                if let Some(value) = self.read_attribute(&request) {
156                    serialized.insert(include.association().to_string(), value);
157                }
158            }
159        }
160
161        serialized
162    }
163
164    /// Returns a JSON value for the model, optionally wrapped under a root key.
165    fn as_json(&self, options: Option<SerializationOptions>) -> Value {
166        let root = options.as_ref().and_then(|opts| opts.root.clone());
167        let object = Value::Object(hash_to_json_map(self.serializable_hash(options)));
168
169        match root {
170            Some(root_key) => {
171                let mut wrapped = Map::with_capacity(1);
172                wrapped.insert(root_key, object);
173                Value::Object(wrapped)
174            }
175            None => object,
176        }
177    }
178
179    /// Returns a JSON string for the model.
180    fn to_json(&self, options: Option<SerializationOptions>) -> String {
181        let root = options.as_ref().and_then(|opts| opts.root.clone());
182        let object = self.serializable_hash(options);
183
184        match root {
185            Some(root_key) => serde_json::to_string(&IndexMap::from([(
186                root_key,
187                Value::Object(hash_to_json_map(object)),
188            )]))
189            .expect("root-wrapped serialization should succeed"),
190            None => serde_json::to_string(&object).expect("serialization should succeed"),
191        }
192    }
193}
194
195impl<T> Serialization for T where T: Attributes {}
196
197fn filter_attribute_names(attribute_names: &[&str], options: &SerializationOptions) -> Vec<String> {
198    if let Some(only) = options.only.as_ref() {
199        let mut selected = Vec::with_capacity(only.len());
200
201        for candidate in only {
202            if attribute_names.contains(&candidate.as_str()) && !selected.contains(candidate) {
203                selected.push(candidate.clone());
204            }
205        }
206
207        return selected;
208    }
209
210    attribute_names
211        .iter()
212        .filter(|name| {
213            options
214                .except
215                .as_ref()
216                .is_none_or(|except| except.iter().all(|candidate| candidate != *name))
217        })
218        .map(|name| (*name).to_string())
219        .collect()
220}
221
222fn hash_to_json_map(hash: SerializationMap) -> Map<String, Value> {
223    hash.into_iter().collect()
224}
225
226#[cfg(test)]
227mod tests {
228    use std::collections::HashMap;
229
230    use serde_json::{Value, json};
231
232    use super::{
233        Serialization, SerializationInclude, SerializationMap, SerializationOptions,
234        association_serialization_key, parse_association_serialization_key,
235    };
236    use crate::{AttributeError, Attributes};
237
238    #[derive(Debug, Clone)]
239    struct TestUser {
240        id: u64,
241        name: String,
242        email: String,
243    }
244
245    impl Attributes for TestUser {
246        fn attribute_names() -> &'static [&'static str] {
247            &["id", "name", "email"]
248        }
249
250        fn read_attribute(&self, name: &str) -> Option<Value> {
251            match name {
252                "id" => Some(Value::from(self.id)),
253                "name" => Some(Value::String(self.name.clone())),
254                "email" => Some(Value::String(self.email.clone())),
255                "display_name" => Some(Value::String(format!("{} <{}>", self.name, self.email))),
256                _ => None,
257            }
258        }
259
260        fn write_attribute(&mut self, name: &str, value: Value) -> Result<(), AttributeError> {
261            match (name, value) {
262                ("id", Value::Number(number)) => {
263                    let id = number
264                        .as_u64()
265                        .ok_or_else(|| AttributeError::TypeMismatch {
266                            attribute: "id".to_string(),
267                            expected: "u64".to_string(),
268                            actual: "number".to_string(),
269                        })?;
270                    self.id = id;
271                    Ok(())
272                }
273                ("name", Value::String(name)) => {
274                    self.name = name;
275                    Ok(())
276                }
277                ("email", Value::String(email)) => {
278                    self.email = email;
279                    Ok(())
280                }
281                ("id", other) => Err(AttributeError::TypeMismatch {
282                    attribute: "id".to_string(),
283                    expected: "u64".to_string(),
284                    actual: other.to_string(),
285                }),
286                ("name", other) => Err(AttributeError::TypeMismatch {
287                    attribute: "name".to_string(),
288                    expected: "string".to_string(),
289                    actual: other.to_string(),
290                }),
291                ("email", other) => Err(AttributeError::TypeMismatch {
292                    attribute: "email".to_string(),
293                    expected: "string".to_string(),
294                    actual: other.to_string(),
295                }),
296                (unknown, _) => Err(AttributeError::UnknownAttribute(unknown.to_string())),
297            }
298        }
299
300        fn assign_attributes(
301            &mut self,
302            attrs: HashMap<String, Value>,
303        ) -> Result<(), AttributeError> {
304            for (name, value) in attrs {
305                self.write_attribute(&name, value)?;
306            }
307            Ok(())
308        }
309
310        fn attributes(&self) -> HashMap<String, Value> {
311            let mut attributes = HashMap::new();
312            attributes.insert("id".to_string(), Value::from(self.id));
313            attributes.insert("name".to_string(), Value::String(self.name.clone()));
314            attributes.insert("email".to_string(), Value::String(self.email.clone()));
315            attributes
316        }
317    }
318
319    fn test_user() -> TestUser {
320        TestUser {
321            id: 1,
322            name: "Alice".to_string(),
323            email: "alice@example.com".to_string(),
324        }
325    }
326
327    #[derive(Debug, Clone)]
328    struct TestAddress {
329        street: String,
330        city: String,
331        state: String,
332        zip: u32,
333    }
334
335    impl Attributes for TestAddress {
336        fn attribute_names() -> &'static [&'static str] {
337            &["street", "city", "state", "zip"]
338        }
339
340        fn read_attribute(&self, name: &str) -> Option<Value> {
341            match name {
342                "street" => Some(json!(self.street)),
343                "city" => Some(json!(self.city)),
344                "state" => Some(json!(self.state)),
345                "zip" => Some(json!(self.zip)),
346                _ => None,
347            }
348        }
349
350        fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
351            Err(AttributeError::UnknownAttribute(name.to_string()))
352        }
353
354        fn attributes(&self) -> HashMap<String, Value> {
355            HashMap::from([
356                ("street".to_string(), json!(self.street)),
357                ("city".to_string(), json!(self.city)),
358                ("state".to_string(), json!(self.state)),
359                ("zip".to_string(), json!(self.zip)),
360            ])
361        }
362    }
363
364    #[derive(Debug, Clone)]
365    struct FriendList {
366        friends: Vec<SerializationUser>,
367    }
368
369    #[derive(Debug, Clone)]
370    enum FriendsAssociation {
371        Direct(Vec<SerializationUser>),
372        Ary(FriendList),
373    }
374
375    #[derive(Debug, Clone)]
376    struct SerializationUser {
377        name: String,
378        email: String,
379        gender: String,
380        address: Option<TestAddress>,
381        friends: FriendsAssociation,
382    }
383
384    impl SerializationUser {
385        fn new(name: &str, email: &str, gender: &str) -> Self {
386            Self {
387                name: name.to_string(),
388                email: email.to_string(),
389                gender: gender.to_string(),
390                address: None,
391                friends: FriendsAssociation::Direct(vec![]),
392            }
393        }
394    }
395
396    impl Attributes for SerializationUser {
397        fn attribute_names() -> &'static [&'static str] {
398            &["name", "email", "gender"]
399        }
400
401        fn read_attribute(&self, name: &str) -> Option<Value> {
402            if let Some((association, options)) = parse_association_serialization_key(name) {
403                return match association.as_str() {
404                    "address" => self
405                        .address
406                        .as_ref()
407                        .map(|address| serialize_record(address, options)),
408                    "friends" => Some(match &self.friends {
409                        FriendsAssociation::Direct(friends) => serialize_records(friends, options),
410                        FriendsAssociation::Ary(list) => serialize_records(&list.friends, options),
411                    }),
412                    _ => None,
413                };
414            }
415
416            match name {
417                "name" => Some(json!(self.name)),
418                "email" => Some(json!(self.email)),
419                "gender" => Some(json!(self.gender)),
420                "full_name" => Some(json!(format!("{} <{}>", self.name, self.email))),
421                _ => None,
422            }
423        }
424
425        fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
426            Err(AttributeError::UnknownAttribute(name.to_string()))
427        }
428
429        fn attributes(&self) -> HashMap<String, Value> {
430            HashMap::from([
431                ("name".to_string(), json!(self.name)),
432                ("email".to_string(), json!(self.email)),
433                ("gender".to_string(), json!(self.gender)),
434            ])
435        }
436    }
437
438    fn serialize_record<T: Serialization>(record: &T, options: SerializationOptions) -> Value {
439        Value::Object(super::hash_to_json_map(
440            record.serializable_hash(Some(options)),
441        ))
442    }
443
444    fn serialize_records<T: Serialization>(records: &[T], options: SerializationOptions) -> Value {
445        Value::Array(
446            records
447                .iter()
448                .map(|record| serialize_record(record, options.clone()))
449                .collect(),
450        )
451    }
452
453    fn serialization_friends() -> Vec<SerializationUser> {
454        vec![
455            SerializationUser::new("Joe", "joe@example.com", "male"),
456            SerializationUser::new("Sue", "sue@example.com", "female"),
457        ]
458    }
459
460    fn serialization_user_shallow() -> SerializationUser {
461        SerializationUser::new("David", "david@example.com", "male")
462    }
463
464    fn serialization_user() -> SerializationUser {
465        let mut user = serialization_user_shallow();
466        user.address = Some(TestAddress {
467            street: "123 Lane".to_string(),
468            city: "Springfield".to_string(),
469            state: "CA".to_string(),
470            zip: 11111,
471        });
472        user.friends = FriendsAssociation::Direct(serialization_friends());
473        user
474    }
475
476    #[test]
477    fn serializes_all_attributes_by_default() {
478        let user = test_user();
479
480        let serialized = user.serializable_hash(None);
481
482        assert_eq!(serialized.get("id"), Some(&json!(1)));
483        assert_eq!(serialized.get("name"), Some(&json!("Alice")));
484        assert_eq!(serialized.get("email"), Some(&json!("alice@example.com")));
485    }
486
487    #[test]
488    fn only_filters_attributes_and_wins_over_except() {
489        let user = test_user();
490        let options = SerializationOptions::new()
491            .only(vec!["name".to_string(), "email".to_string()])
492            .except(vec!["email".to_string()]);
493
494        let serialized = user.serializable_hash(Some(options));
495
496        assert_eq!(serialized.len(), 2);
497        assert!(serialized.contains_key("name"));
498        assert!(serialized.contains_key("email"));
499        assert!(!serialized.contains_key("id"));
500    }
501
502    #[test]
503    fn except_excludes_attributes() {
504        let user = test_user();
505        let serialized = user.serializable_hash(Some(
506            SerializationOptions::new().except(vec!["email".to_string()]),
507        ));
508
509        assert!(serialized.contains_key("id"));
510        assert!(serialized.contains_key("name"));
511        assert!(!serialized.contains_key("email"));
512    }
513
514    #[test]
515    fn methods_add_extra_values_when_available() {
516        let user = test_user();
517        let serialized = user.serializable_hash(Some(
518            SerializationOptions::new().methods(vec!["display_name".to_string()]),
519        ));
520
521        assert_eq!(
522            serialized.get("display_name"),
523            Some(&json!("Alice <alice@example.com>"))
524        );
525    }
526
527    #[test]
528    fn as_json_wraps_under_root_key() {
529        let user = test_user();
530        let value = user.as_json(Some(SerializationOptions::new().root("user".to_string())));
531
532        assert_eq!(
533            value,
534            json!({
535                "user": {
536                    "id": 1,
537                    "name": "Alice",
538                    "email": "alice@example.com"
539                }
540            })
541        );
542    }
543
544    #[test]
545    fn to_json_returns_valid_json() {
546        let user = test_user();
547        let json = user.to_json(Some(
548            SerializationOptions::new().only(vec!["name".to_string()]),
549        ));
550
551        assert_eq!(json, "{\"name\":\"Alice\"}");
552    }
553    #[test]
554    fn association_request_keys_round_trip_nested_options() {
555        let options = SerializationOptions::new()
556            .only(vec!["name".to_string()])
557            .methods(vec!["full_name".to_string()]);
558        let key = association_serialization_key("friends", Some(options.clone()));
559
560        assert_eq!(
561            parse_association_serialization_key(&key),
562            Some(("friends".to_string(), options))
563        );
564    }
565
566    #[test]
567    fn builder_methods_store_requested_options() {
568        let options = SerializationOptions::new()
569            .only(vec!["name".to_string()])
570            .except(vec!["email".to_string()])
571            .methods(vec!["display_name".to_string()])
572            .include(vec![SerializationInclude::named("address")])
573            .root("user".to_string());
574
575        assert_eq!(options.only, Some(vec!["name".to_string()]));
576        assert_eq!(options.except, Some(vec!["email".to_string()]));
577        assert_eq!(options.methods, Some(vec!["display_name".to_string()]));
578        assert_eq!(
579            options.include,
580            Some(vec![SerializationInclude::named("address")])
581        );
582        assert_eq!(options.root, Some("user".to_string()));
583    }
584
585    #[test]
586    fn methods_ignore_unknown_dynamic_names() {
587        let user = test_user();
588        let serialized = user.serializable_hash(Some(
589            SerializationOptions::new().methods(vec!["missing".to_string()]),
590        ));
591
592        assert!(!serialized.contains_key("missing"));
593    }
594
595    #[test]
596    fn as_json_without_root_returns_plain_object() {
597        let user = test_user();
598        assert_eq!(
599            user.as_json(None),
600            json!({
601                "id": 1,
602                "name": "Alice",
603                "email": "alice@example.com"
604            })
605        );
606    }
607
608    #[test]
609    fn only_with_unknown_attributes_returns_empty_hash() {
610        let user = test_user();
611        let serialized = user.serializable_hash(Some(
612            SerializationOptions::new().only(vec!["missing".to_string()]),
613        ));
614
615        assert!(serialized.is_empty());
616    }
617
618    #[test]
619    fn except_can_remove_every_attribute() {
620        let user = test_user();
621        let serialized = user.serializable_hash(Some(SerializationOptions::new().except(vec![
622            "id".to_string(),
623            "name".to_string(),
624            "email".to_string(),
625        ])));
626
627        assert!(serialized.is_empty());
628    }
629
630    #[test]
631    fn to_json_preserves_root_wrapper() {
632        let user = test_user();
633        let json = user.to_json(Some(SerializationOptions::new().root("user".to_string())));
634
635        assert_eq!(
636            serde_json::from_str::<Value>(&json).expect("root-wrapped JSON should parse"),
637            json!({
638                "user": {
639                    "id": 1,
640                    "name": "Alice",
641                    "email": "alice@example.com"
642                }
643            })
644        );
645    }
646
647    #[test]
648    fn unreadable_declared_attributes_serialize_as_null() {
649        #[derive(Debug, Clone)]
650        struct PartialRead;
651
652        impl Attributes for PartialRead {
653            fn attribute_names() -> &'static [&'static str] {
654                &["visible", "hidden"]
655            }
656
657            fn read_attribute(&self, name: &str) -> Option<Value> {
658                match name {
659                    "visible" => Some(json!("value")),
660                    _ => None,
661                }
662            }
663
664            fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
665                Err(AttributeError::UnknownAttribute(name.to_string()))
666            }
667
668            fn attributes(&self) -> HashMap<String, Value> {
669                HashMap::from([
670                    ("visible".to_string(), json!("value")),
671                    ("hidden".to_string(), Value::Null),
672                ])
673            }
674        }
675
676        let value = PartialRead.as_json(None);
677        assert_eq!(value, json!({"visible": "value", "hidden": null}));
678    }
679
680    #[test]
681    fn serialization_options_start_empty() {
682        let options = SerializationOptions::new();
683
684        assert_eq!(options.only, None);
685        assert_eq!(options.except, None);
686        assert_eq!(options.methods, None);
687        assert_eq!(options.include, None);
688        assert_eq!(options.root, None);
689    }
690
691    #[test]
692    fn only_with_a_single_attribute_returns_that_attribute() {
693        let user = test_user();
694        let serialized = user.serializable_hash(Some(
695            SerializationOptions::new().only(vec!["name".to_string()]),
696        ));
697
698        assert_eq!(
699            serialized,
700            SerializationMap::from([("name".to_string(), json!("Alice"))])
701        );
702    }
703
704    #[test]
705    fn except_with_an_unknown_attribute_keeps_all_attributes() {
706        let user = test_user();
707        let serialized = user.serializable_hash(Some(
708            SerializationOptions::new().except(vec!["missing".to_string()]),
709        ));
710
711        assert_eq!(serialized, user.serializable_hash(None));
712    }
713
714    #[test]
715    fn empty_except_keeps_all_attributes() {
716        let user = test_user();
717        let serialized = user.serializable_hash(Some(SerializationOptions::new().except(vec![])));
718
719        assert_eq!(serialized, user.serializable_hash(None));
720    }
721
722    #[test]
723    fn empty_only_returns_an_empty_hash() {
724        let user = test_user();
725        let serialized = user.serializable_hash(Some(SerializationOptions::new().only(vec![])));
726
727        assert!(serialized.is_empty());
728    }
729
730    #[test]
731    fn duplicate_only_attributes_do_not_duplicate_entries() {
732        let user = test_user();
733        let serialized = user.serializable_hash(Some(
734            SerializationOptions::new().only(vec!["name".to_string(), "name".to_string()]),
735        ));
736
737        assert_eq!(serialized.len(), 1);
738        assert_eq!(serialized.get("name"), Some(&json!("Alice")));
739    }
740
741    #[test]
742    fn methods_can_be_returned_without_base_attributes() {
743        let user = test_user();
744        let serialized = user.serializable_hash(Some(
745            SerializationOptions::new()
746                .only(vec![])
747                .methods(vec!["display_name".to_string()]),
748        ));
749
750        assert_eq!(
751            serialized,
752            SerializationMap::from([(
753                "display_name".to_string(),
754                json!("Alice <alice@example.com>"),
755            )])
756        );
757    }
758
759    #[test]
760    fn mixed_known_and_unknown_methods_include_only_known_values() {
761        let user = test_user();
762        let serialized = user.serializable_hash(Some(
763            SerializationOptions::new()
764                .methods(vec!["display_name".to_string(), "missing".to_string()]),
765        ));
766
767        assert_eq!(
768            serialized.get("display_name"),
769            Some(&json!("Alice <alice@example.com>"))
770        );
771        assert!(!serialized.contains_key("missing"));
772    }
773
774    #[test]
775    fn methods_can_reintroduce_attributes_filtered_out_by_except() {
776        let user = test_user();
777        let serialized = user.serializable_hash(Some(
778            SerializationOptions::new()
779                .except(vec![
780                    "id".to_string(),
781                    "name".to_string(),
782                    "email".to_string(),
783                ])
784                .methods(vec!["name".to_string()]),
785        ));
786
787        assert_eq!(
788            serialized,
789            SerializationMap::from([("name".to_string(), json!("Alice"))])
790        );
791    }
792
793    #[test]
794    fn as_json_without_root_can_include_method_values() {
795        let user = test_user();
796        let value = user.as_json(Some(
797            SerializationOptions::new().methods(vec!["display_name".to_string()]),
798        ));
799
800        assert_eq!(
801            value,
802            json!({
803                "id": 1,
804                "name": "Alice",
805                "email": "alice@example.com",
806                "display_name": "Alice <alice@example.com>"
807            })
808        );
809    }
810
811    #[test]
812    fn as_json_with_root_can_include_method_values() {
813        let user = test_user();
814        let value = user.as_json(Some(
815            SerializationOptions::new()
816                .methods(vec!["display_name".to_string()])
817                .root("user".to_string()),
818        ));
819
820        assert_eq!(
821            value,
822            json!({
823                "user": {
824                    "id": 1,
825                    "name": "Alice",
826                    "email": "alice@example.com",
827                    "display_name": "Alice <alice@example.com>"
828                }
829            })
830        );
831    }
832
833    #[test]
834    fn to_json_without_options_matches_as_json_output() {
835        let user = test_user();
836
837        assert_eq!(
838            serde_json::from_str::<Value>(&user.to_json(None)).expect("JSON should parse"),
839            user.as_json(None)
840        );
841    }
842
843    #[test]
844    fn to_json_with_root_and_methods_round_trips_through_json_parser() {
845        let user = test_user();
846        let json = user.to_json(Some(
847            SerializationOptions::new()
848                .methods(vec!["display_name".to_string()])
849                .root("user".to_string()),
850        ));
851
852        assert_eq!(
853            serde_json::from_str::<Value>(&json).expect("JSON should parse"),
854            json!({
855                "user": {
856                    "id": 1,
857                    "name": "Alice",
858                    "email": "alice@example.com",
859                    "display_name": "Alice <alice@example.com>"
860                }
861            })
862        );
863    }
864
865    #[test]
866    fn selected_unreadable_attributes_still_serialize_as_null() {
867        #[derive(Debug, Clone)]
868        struct HiddenOnly;
869
870        impl Attributes for HiddenOnly {
871            fn attribute_names() -> &'static [&'static str] {
872                &["hidden"]
873            }
874
875            fn read_attribute(&self, _name: &str) -> Option<Value> {
876                None
877            }
878
879            fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
880                Err(AttributeError::UnknownAttribute(name.to_string()))
881            }
882
883            fn attributes(&self) -> HashMap<String, Value> {
884                HashMap::from([("hidden".to_string(), Value::Null)])
885            }
886        }
887
888        let serialized = HiddenOnly.serializable_hash(Some(
889            SerializationOptions::new().only(vec!["hidden".to_string()]),
890        ));
891
892        assert_eq!(
893            serialized,
894            SerializationMap::from([("hidden".to_string(), Value::Null)])
895        );
896    }
897
898    #[test]
899    fn as_json_allows_an_empty_root_key() {
900        let user = test_user();
901        let value = user.as_json(Some(SerializationOptions::new().root(String::new())));
902
903        assert_eq!(
904            value,
905            json!({
906                "": {
907                    "id": 1,
908                    "name": "Alice",
909                    "email": "alice@example.com"
910                }
911            })
912        );
913    }
914
915    #[test]
916    fn to_json_serializes_nulls_for_unreadable_declared_attributes() {
917        #[derive(Debug, Clone)]
918        struct HiddenOnly;
919
920        impl Attributes for HiddenOnly {
921            fn attribute_names() -> &'static [&'static str] {
922                &["hidden"]
923            }
924
925            fn read_attribute(&self, _name: &str) -> Option<Value> {
926                None
927            }
928
929            fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
930                Err(AttributeError::UnknownAttribute(name.to_string()))
931            }
932
933            fn attributes(&self) -> HashMap<String, Value> {
934                HashMap::from([("hidden".to_string(), Value::Null)])
935            }
936        }
937
938        assert_eq!(
939            serde_json::from_str::<Value>(&HiddenOnly.to_json(None)).expect("JSON should parse"),
940            json!({"hidden": null})
941        );
942    }
943
944    #[test]
945    fn methods_can_override_existing_attribute_values() {
946        #[derive(Debug)]
947        struct OverridableUser {
948            name_reads: std::cell::RefCell<usize>,
949        }
950
951        impl Attributes for OverridableUser {
952            fn attribute_names() -> &'static [&'static str] {
953                &["name"]
954            }
955
956            fn read_attribute(&self, name: &str) -> Option<Value> {
957                match name {
958                    "name" => {
959                        let mut reads = self.name_reads.borrow_mut();
960                        *reads += 1;
961
962                        Some(if *reads == 1 {
963                            json!("Alice")
964                        } else {
965                            json!("Alicia")
966                        })
967                    }
968                    _ => None,
969                }
970            }
971
972            fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
973                Err(AttributeError::UnknownAttribute(name.to_string()))
974            }
975
976            fn attributes(&self) -> HashMap<String, Value> {
977                HashMap::from([("name".to_string(), json!("Alice"))])
978            }
979        }
980
981        let user = OverridableUser {
982            name_reads: std::cell::RefCell::new(0),
983        };
984        let serialized = user.serializable_hash(Some(
985            SerializationOptions::new().methods(vec!["name".to_string()]),
986        ));
987
988        assert_eq!(
989            serialized,
990            SerializationMap::from([("name".to_string(), json!("Alicia"))])
991        );
992    }
993
994    #[test]
995    fn root_does_not_affect_serializable_hash_when_only_and_methods_are_used() {
996        let user = test_user();
997        let serialized = user.serializable_hash(Some(
998            SerializationOptions::new()
999                .only(vec![])
1000                .methods(vec!["display_name".to_string()])
1001                .root("user".to_string()),
1002        ));
1003
1004        assert_eq!(
1005            serialized,
1006            SerializationMap::from([(
1007                "display_name".to_string(),
1008                json!("Alice <alice@example.com>"),
1009            )])
1010        );
1011        assert!(!serialized.contains_key("user"));
1012    }
1013
1014    #[test]
1015    fn to_json_with_only_empty_and_root_round_trips_to_an_empty_object() {
1016        let user = test_user();
1017        let json = user.to_json(Some(
1018            SerializationOptions::new()
1019                .only(vec![])
1020                .root("user".to_string()),
1021        ));
1022
1023        assert_eq!(
1024            serde_json::from_str::<Value>(&json).expect("JSON should parse"),
1025            json!({"user": {}})
1026        );
1027    }
1028
1029    #[test]
1030    fn to_json_with_only_empty_methods_and_root_round_trips_to_a_methods_only_object() {
1031        let user = test_user();
1032        let json = user.to_json(Some(
1033            SerializationOptions::new()
1034                .only(vec![])
1035                .methods(vec!["display_name".to_string()])
1036                .root("user".to_string()),
1037        ));
1038
1039        assert_eq!(
1040            serde_json::from_str::<Value>(&json).expect("JSON should parse"),
1041            json!({
1042                "user": {
1043                    "display_name": "Alice <alice@example.com>"
1044                }
1045            })
1046        );
1047    }
1048
1049    #[test]
1050    fn test_method_serializable_hash_should_work() {
1051        let user = test_user();
1052
1053        assert_eq!(
1054            user.serializable_hash(None),
1055            SerializationMap::from([
1056                ("id".to_string(), json!(1)),
1057                ("name".to_string(), json!("Alice")),
1058                ("email".to_string(), json!("alice@example.com")),
1059            ])
1060        );
1061    }
1062
1063    #[test]
1064    fn test_method_serializable_hash_should_work_with_only_option() {
1065        let user = test_user();
1066
1067        assert_eq!(
1068            user.serializable_hash(Some(
1069                SerializationOptions::new().only(vec!["name".to_string()]),
1070            )),
1071            SerializationMap::from([("name".to_string(), json!("Alice"))])
1072        );
1073    }
1074
1075    #[test]
1076    fn test_method_serializable_hash_should_work_with_only_option_with_order_of_given_keys() {
1077        let user = test_user();
1078        let serialized = user.serializable_hash(Some(
1079            SerializationOptions::new().only(vec!["name".to_string(), "email".to_string()]),
1080        ));
1081
1082        assert_eq!(
1083            serialized.keys().cloned().collect::<Vec<_>>(),
1084            vec!["name".to_string(), "email".to_string()]
1085        );
1086    }
1087
1088    #[test]
1089    fn test_method_serializable_hash_should_work_with_except_option() {
1090        let user = test_user();
1091
1092        assert_eq!(
1093            user.serializable_hash(Some(
1094                SerializationOptions::new().except(vec!["name".to_string()]),
1095            )),
1096            SerializationMap::from([
1097                ("id".to_string(), json!(1)),
1098                ("email".to_string(), json!("alice@example.com")),
1099            ])
1100        );
1101    }
1102
1103    #[test]
1104    fn test_method_serializable_hash_should_work_with_methods_option() {
1105        let user = test_user();
1106
1107        assert_eq!(
1108            user.serializable_hash(Some(
1109                SerializationOptions::new().methods(vec!["display_name".to_string()]),
1110            )),
1111            SerializationMap::from([
1112                ("id".to_string(), json!(1)),
1113                ("name".to_string(), json!("Alice")),
1114                ("email".to_string(), json!("alice@example.com")),
1115                (
1116                    "display_name".to_string(),
1117                    json!("Alice <alice@example.com>"),
1118                ),
1119            ])
1120        );
1121    }
1122
1123    #[test]
1124    fn test_method_serializable_hash_should_work_with_only_and_methods() {
1125        let user = test_user();
1126
1127        assert_eq!(
1128            user.serializable_hash(Some(
1129                SerializationOptions::new()
1130                    .only(vec![])
1131                    .methods(vec!["display_name".to_string()]),
1132            )),
1133            SerializationMap::from([(
1134                "display_name".to_string(),
1135                json!("Alice <alice@example.com>"),
1136            )])
1137        );
1138    }
1139
1140    #[test]
1141    fn test_method_serializable_hash_should_work_with_except_and_methods() {
1142        let user = test_user();
1143
1144        assert_eq!(
1145            user.serializable_hash(Some(
1146                SerializationOptions::new()
1147                    .except(vec!["name".to_string()])
1148                    .methods(vec!["display_name".to_string()]),
1149            )),
1150            SerializationMap::from([
1151                ("id".to_string(), json!(1)),
1152                ("email".to_string(), json!("alice@example.com")),
1153                (
1154                    "display_name".to_string(),
1155                    json!("Alice <alice@example.com>"),
1156                ),
1157            ])
1158        );
1159    }
1160
1161    #[test]
1162    #[ignore = "Rails-specific: Rust serialization skips unknown method names instead of raising an error"]
1163    fn test_should_raise_no_method_error_for_non_existing_method() {}
1164
1165    #[test]
1166    #[ignore = "Rails-specific: the Serialization trait has no read_attribute_for_serialization override hook"]
1167    fn test_should_use_read_attribute_for_serialization() {}
1168
1169    #[test]
1170    fn test_include_option_with_singular_association() {
1171        let user = serialization_user();
1172        let serialized = user.serializable_hash(Some(
1173            SerializationOptions::new().include(vec![SerializationInclude::named("address")]),
1174        ));
1175
1176        assert_eq!(
1177            serialized,
1178            SerializationMap::from([
1179                ("name".to_string(), json!("David")),
1180                ("email".to_string(), json!("david@example.com")),
1181                ("gender".to_string(), json!("male")),
1182                (
1183                    "address".to_string(),
1184                    json!({
1185                        "street": "123 Lane",
1186                        "city": "Springfield",
1187                        "state": "CA",
1188                        "zip": 11111
1189                    }),
1190                ),
1191            ])
1192        );
1193    }
1194
1195    #[test]
1196    fn test_include_option_with_plural_association() {
1197        let user = serialization_user();
1198        let serialized = user.serializable_hash(Some(
1199            SerializationOptions::new().include(vec![SerializationInclude::named("friends")]),
1200        ));
1201
1202        assert_eq!(
1203            serialized,
1204            SerializationMap::from([
1205                ("name".to_string(), json!("David")),
1206                ("email".to_string(), json!("david@example.com")),
1207                ("gender".to_string(), json!("male")),
1208                (
1209                    "friends".to_string(),
1210                    json!([
1211                        {"name": "Joe", "email": "joe@example.com", "gender": "male"},
1212                        {"name": "Sue", "email": "sue@example.com", "gender": "female"}
1213                    ]),
1214                ),
1215            ])
1216        );
1217    }
1218
1219    #[test]
1220    fn test_include_option_with_empty_association() {
1221        let mut user = serialization_user();
1222        user.friends = FriendsAssociation::Direct(vec![]);
1223
1224        let serialized = user.serializable_hash(Some(
1225            SerializationOptions::new().include(vec![SerializationInclude::named("friends")]),
1226        ));
1227
1228        assert_eq!(
1229            serialized,
1230            SerializationMap::from([
1231                ("name".to_string(), json!("David")),
1232                ("email".to_string(), json!("david@example.com")),
1233                ("gender".to_string(), json!("male")),
1234                ("friends".to_string(), json!([])),
1235            ])
1236        );
1237    }
1238
1239    #[test]
1240    fn test_include_option_with_ary() {
1241        let mut user = serialization_user();
1242        user.friends = FriendsAssociation::Ary(FriendList {
1243            friends: serialization_friends(),
1244        });
1245
1246        let serialized = user.serializable_hash(Some(
1247            SerializationOptions::new().include(vec![SerializationInclude::named("friends")]),
1248        ));
1249
1250        assert_eq!(
1251            serialized,
1252            SerializationMap::from([
1253                ("name".to_string(), json!("David")),
1254                ("email".to_string(), json!("david@example.com")),
1255                ("gender".to_string(), json!("male")),
1256                (
1257                    "friends".to_string(),
1258                    json!([
1259                        {"name": "Joe", "email": "joe@example.com", "gender": "male"},
1260                        {"name": "Sue", "email": "sue@example.com", "gender": "female"}
1261                    ]),
1262                ),
1263            ])
1264        );
1265    }
1266
1267    #[test]
1268    fn test_multiple_includes() {
1269        let user = serialization_user();
1270        let serialized = user.serializable_hash(Some(SerializationOptions::new().include(vec![
1271            SerializationInclude::named("address"),
1272            SerializationInclude::named("friends"),
1273        ])));
1274
1275        assert_eq!(
1276            serialized,
1277            SerializationMap::from([
1278                ("name".to_string(), json!("David")),
1279                ("email".to_string(), json!("david@example.com")),
1280                ("gender".to_string(), json!("male")),
1281                (
1282                    "address".to_string(),
1283                    json!({
1284                        "street": "123 Lane",
1285                        "city": "Springfield",
1286                        "state": "CA",
1287                        "zip": 11111
1288                    }),
1289                ),
1290                (
1291                    "friends".to_string(),
1292                    json!([
1293                        {"name": "Joe", "email": "joe@example.com", "gender": "male"},
1294                        {"name": "Sue", "email": "sue@example.com", "gender": "female"}
1295                    ]),
1296                ),
1297            ])
1298        );
1299    }
1300
1301    #[test]
1302    fn test_include_with_options() {
1303        let user = serialization_user();
1304        let serialized = user.serializable_hash(Some(SerializationOptions::new().include(vec![
1305            SerializationInclude::with_options(
1306                "address",
1307                SerializationOptions::new().only(vec!["street".to_string()]),
1308            ),
1309        ])));
1310
1311        assert_eq!(
1312            serialized,
1313            SerializationMap::from([
1314                ("name".to_string(), json!("David")),
1315                ("email".to_string(), json!("david@example.com")),
1316                ("gender".to_string(), json!("male")),
1317                ("address".to_string(), json!({"street": "123 Lane"})),
1318            ])
1319        );
1320    }
1321
1322    #[test]
1323    fn test_include_with_nested_methods() {
1324        let user = serialization_user();
1325        let serialized = user.serializable_hash(Some(SerializationOptions::new().include(vec![
1326            SerializationInclude::with_options(
1327                "friends",
1328                SerializationOptions::new()
1329                    .only(vec!["name".to_string()])
1330                    .methods(vec!["full_name".to_string()]),
1331            ),
1332        ])));
1333
1334        assert_eq!(
1335            serialized,
1336            SerializationMap::from([
1337                ("name".to_string(), json!("David")),
1338                ("email".to_string(), json!("david@example.com")),
1339                ("gender".to_string(), json!("male")),
1340                (
1341                    "friends".to_string(),
1342                    json!([
1343                        {"name": "Joe", "full_name": "Joe <joe@example.com>"},
1344                        {"name": "Sue", "full_name": "Sue <sue@example.com>"}
1345                    ]),
1346                ),
1347            ])
1348        );
1349    }
1350
1351    #[test]
1352    fn test_nested_include() {
1353        let mut user = serialization_user();
1354        if let FriendsAssociation::Direct(friends) = &mut user.friends {
1355            friends[0].friends = FriendsAssociation::Direct(vec![serialization_user_shallow()]);
1356        }
1357
1358        let serialized = user.serializable_hash(Some(SerializationOptions::new().include(vec![
1359            SerializationInclude::with_options(
1360                "friends",
1361                SerializationOptions::new().include(vec![SerializationInclude::named("friends")]),
1362            ),
1363        ])));
1364
1365        assert_eq!(
1366            serialized,
1367            SerializationMap::from([
1368                ("name".to_string(), json!("David")),
1369                ("email".to_string(), json!("david@example.com")),
1370                ("gender".to_string(), json!("male")),
1371                (
1372                    "friends".to_string(),
1373                    json!([
1374                        {
1375                            "name": "Joe",
1376                            "email": "joe@example.com",
1377                            "gender": "male",
1378                            "friends": [{"name": "David", "email": "david@example.com", "gender": "male"}]
1379                        },
1380                        {
1381                            "name": "Sue",
1382                            "email": "sue@example.com",
1383                            "gender": "female",
1384                            "friends": []
1385                        }
1386                    ]),
1387                ),
1388            ])
1389        );
1390    }
1391
1392    #[test]
1393    fn test_only_include() {
1394        let user = serialization_user();
1395        let serialized = user.serializable_hash(Some(
1396            SerializationOptions::new()
1397                .only(vec!["name".to_string()])
1398                .include(vec![SerializationInclude::with_options(
1399                    "friends",
1400                    SerializationOptions::new().only(vec!["name".to_string()]),
1401                )]),
1402        ));
1403
1404        assert_eq!(
1405            serialized,
1406            SerializationMap::from([
1407                ("name".to_string(), json!("David")),
1408                (
1409                    "friends".to_string(),
1410                    json!([
1411                        {"name": "Joe"},
1412                        {"name": "Sue"}
1413                    ]),
1414                ),
1415            ])
1416        );
1417    }
1418
1419    #[test]
1420    fn test_except_include() {
1421        let user = serialization_user();
1422        let serialized = user.serializable_hash(Some(
1423            SerializationOptions::new()
1424                .except(vec!["gender".to_string()])
1425                .include(vec![SerializationInclude::with_options(
1426                    "friends",
1427                    SerializationOptions::new().except(vec!["gender".to_string()]),
1428                )]),
1429        ));
1430
1431        assert_eq!(
1432            serialized,
1433            SerializationMap::from([
1434                ("name".to_string(), json!("David")),
1435                ("email".to_string(), json!("david@example.com")),
1436                (
1437                    "friends".to_string(),
1438                    json!([
1439                        {"name": "Joe", "email": "joe@example.com"},
1440                        {"name": "Sue", "email": "sue@example.com"}
1441                    ]),
1442                ),
1443            ])
1444        );
1445    }
1446
1447    #[test]
1448    fn test_multiple_includes_with_options() {
1449        let user = serialization_user();
1450        let serialized = user.serializable_hash(Some(SerializationOptions::new().include(vec![
1451            SerializationInclude::with_options(
1452                "address",
1453                SerializationOptions::new().only(vec!["street".to_string()]),
1454            ),
1455            SerializationInclude::named("friends"),
1456        ])));
1457
1458        assert_eq!(
1459            serialized,
1460            SerializationMap::from([
1461                ("name".to_string(), json!("David")),
1462                ("email".to_string(), json!("david@example.com")),
1463                ("gender".to_string(), json!("male")),
1464                ("address".to_string(), json!({"street": "123 Lane"})),
1465                (
1466                    "friends".to_string(),
1467                    json!([
1468                        {"name": "Joe", "email": "joe@example.com", "gender": "male"},
1469                        {"name": "Sue", "email": "sue@example.com", "gender": "female"}
1470                    ]),
1471                ),
1472            ])
1473        );
1474    }
1475
1476    #[test]
1477    fn test_all_includes_with_options() {
1478        let user = serialization_user();
1479        let serialized = user.serializable_hash(Some(SerializationOptions::new().include(vec![
1480            SerializationInclude::with_options(
1481                "address",
1482                SerializationOptions::new().only(vec!["street".to_string()]),
1483            ),
1484            SerializationInclude::with_options(
1485                "friends",
1486                SerializationOptions::new().only(vec!["name".to_string()]),
1487            ),
1488        ])));
1489
1490        assert_eq!(
1491            serialized,
1492            SerializationMap::from([
1493                ("name".to_string(), json!("David")),
1494                ("email".to_string(), json!("david@example.com")),
1495                ("gender".to_string(), json!("male")),
1496                ("address".to_string(), json!({"street": "123 Lane"})),
1497                (
1498                    "friends".to_string(),
1499                    json!([
1500                        {"name": "Joe"},
1501                        {"name": "Sue"}
1502                    ]),
1503                ),
1504            ])
1505        );
1506    }
1507
1508    #[test]
1509    fn unknown_includes_are_skipped() {
1510        let user = serialization_user();
1511        let serialized = user.serializable_hash(Some(
1512            SerializationOptions::new().include(vec![SerializationInclude::named("missing")]),
1513        ));
1514
1515        assert_eq!(
1516            serialized,
1517            SerializationMap::from([
1518                ("name".to_string(), json!("David")),
1519                ("email".to_string(), json!("david@example.com")),
1520                ("gender".to_string(), json!("male")),
1521            ])
1522        );
1523    }
1524    #[test]
1525    fn test_multiple_includes_preserve_requested_association_order() {
1526        let user = serialization_user();
1527        let serialized = user.serializable_hash(Some(SerializationOptions::new().include(vec![
1528            SerializationInclude::named("friends"),
1529            SerializationInclude::named("address"),
1530        ])));
1531
1532        assert_eq!(
1533            serialized.keys().cloned().collect::<Vec<_>>(),
1534            vec![
1535                "name".to_string(),
1536                "email".to_string(),
1537                "gender".to_string(),
1538                "friends".to_string(),
1539                "address".to_string(),
1540            ]
1541        );
1542    }
1543
1544    #[test]
1545    fn methods_are_serialized_before_included_associations() {
1546        let user = serialization_user();
1547        let serialized = user.serializable_hash(Some(
1548            SerializationOptions::new()
1549                .methods(vec!["full_name".to_string()])
1550                .include(vec![SerializationInclude::named("address")]),
1551        ));
1552
1553        assert_eq!(
1554            serialized.keys().cloned().collect::<Vec<_>>(),
1555            vec![
1556                "name".to_string(),
1557                "email".to_string(),
1558                "gender".to_string(),
1559                "full_name".to_string(),
1560                "address".to_string(),
1561            ]
1562        );
1563    }
1564}