Skip to main content

rustrails_model/
api.rs

1use std::collections::HashMap;
2
3use serde_json::Value;
4
5use crate::{AttributeError, Attributes, Conversion, Errors, ModelNaming, Serialization};
6
7/// The minimum viable model interface shared by RustRails model types.
8pub trait Model:
9    Attributes + ModelNaming + Conversion + Serialization + Sized + Send + Sync
10{
11    /// Creates a new instance with default values.
12    fn new() -> Self;
13
14    /// Creates a new instance and applies the provided attributes.
15    fn initialize(attrs: HashMap<String, Value>) -> Result<Self, AttributeError> {
16        let mut model = Self::new();
17        model.assign_attributes(attrs)?;
18        Ok(model)
19    }
20
21    /// Returns `true` when the model considers itself valid.
22    fn is_valid(&self) -> bool {
23        true
24    }
25
26    /// Returns `true` when the model considers itself invalid.
27    fn is_invalid(&self) -> bool {
28        !self.is_valid()
29    }
30
31    /// Returns the model's current error collection.
32    fn errors(&self) -> &Errors;
33
34    /// Returns the mutable error collection for validation updates.
35    fn errors_mut(&mut self) -> &mut Errors;
36
37    /// Validates the model and updates its error collection.
38    fn validate(&mut self) -> bool {
39        self.errors_mut().clear();
40        true
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use std::collections::HashMap;
47
48    use serde_json::{Value, json};
49
50    use super::Model;
51    use crate::{
52        AttributeError, Attributes, Conversion, ErrorType, Errors, ModelNaming, Serialization,
53    };
54
55    #[derive(Debug, Default, Clone)]
56    struct TestUser {
57        id: Option<u64>,
58        name: String,
59        errors: Errors,
60    }
61
62    impl Attributes for TestUser {
63        fn attribute_names() -> &'static [&'static str] {
64            &["id", "name"]
65        }
66
67        fn read_attribute(&self, name: &str) -> Option<Value> {
68            match name {
69                "id" => Some(self.id.map_or(Value::Null, Value::from)),
70                "name" => Some(Value::String(self.name.clone())),
71                _ => None,
72            }
73        }
74
75        fn write_attribute(&mut self, name: &str, value: Value) -> Result<(), AttributeError> {
76            match (name, value) {
77                ("id", Value::Null) => {
78                    self.id = None;
79                    Ok(())
80                }
81                ("id", Value::Number(number)) => {
82                    let id = number
83                        .as_u64()
84                        .ok_or_else(|| AttributeError::TypeMismatch {
85                            attribute: "id".to_string(),
86                            expected: "u64".to_string(),
87                            actual: "number".to_string(),
88                        })?;
89                    self.id = Some(id);
90                    Ok(())
91                }
92                ("name", Value::String(name)) => {
93                    self.name = name;
94                    Ok(())
95                }
96                ("id", other) => Err(AttributeError::TypeMismatch {
97                    attribute: "id".to_string(),
98                    expected: "u64".to_string(),
99                    actual: other.to_string(),
100                }),
101                ("name", other) => Err(AttributeError::TypeMismatch {
102                    attribute: "name".to_string(),
103                    expected: "string".to_string(),
104                    actual: other.to_string(),
105                }),
106                (unknown, _) => Err(AttributeError::UnknownAttribute(unknown.to_string())),
107            }
108        }
109
110        fn assign_attributes(
111            &mut self,
112            attrs: HashMap<String, Value>,
113        ) -> Result<(), AttributeError> {
114            for (name, value) in attrs {
115                self.write_attribute(&name, value)?;
116            }
117            Ok(())
118        }
119
120        fn attributes(&self) -> HashMap<String, Value> {
121            let mut attributes = HashMap::new();
122            attributes.insert("id".to_string(), self.id.map_or(Value::Null, Value::from));
123            attributes.insert("name".to_string(), Value::String(self.name.clone()));
124            attributes
125        }
126    }
127
128    impl ModelNaming for TestUser {}
129    impl Conversion for TestUser {}
130
131    impl Model for TestUser {
132        fn new() -> Self {
133            Self {
134                id: None,
135                name: String::new(),
136                errors: Errors::new(),
137            }
138        }
139
140        fn errors(&self) -> &Errors {
141            &self.errors
142        }
143
144        fn errors_mut(&mut self) -> &mut Errors {
145            &mut self.errors
146        }
147
148        fn is_valid(&self) -> bool {
149            self.errors.is_empty()
150        }
151
152        fn validate(&mut self) -> bool {
153            self.errors.clear();
154            if self.name.trim().is_empty() {
155                self.errors.add("name", ErrorType::Blank, "can't be blank");
156            }
157            self.errors.is_empty()
158        }
159    }
160
161    #[test]
162    fn initialize_assigns_attributes_to_new_model() {
163        let model = TestUser::initialize(HashMap::from([
164            ("id".to_string(), json!(7)),
165            ("name".to_string(), json!("Alice")),
166        ]))
167        .expect("test data should initialize");
168
169        assert_eq!(model.id, Some(7));
170        assert_eq!(model.name, "Alice");
171    }
172
173    #[test]
174    fn validity_defaults_follow_current_errors() {
175        let mut model = TestUser::new();
176
177        assert!(model.is_valid());
178        model
179            .errors_mut()
180            .add("name", ErrorType::Blank, "can't be blank");
181        assert!(model.is_invalid());
182    }
183
184    #[test]
185    fn validate_populates_errors() {
186        let mut model = TestUser::new();
187
188        assert!(!model.validate());
189        assert!(model.is_invalid());
190
191        model.name = "Alice".to_string();
192        assert!(model.validate());
193        assert!(model.is_valid());
194    }
195
196    #[test]
197    fn model_trait_composes_conversion_and_serialization_defaults() {
198        let model = TestUser::initialize(HashMap::from([
199            ("id".to_string(), json!(12)),
200            ("name".to_string(), json!("Alice")),
201        ]))
202        .expect("test data should initialize");
203
204        assert_eq!(model.to_param(), Some("12".to_string()));
205        assert_eq!(model.to_partial_path(), "test_users/test_user");
206        assert_eq!(
207            serde_json::from_str::<Value>(&model.to_json(None)).expect("model JSON should parse"),
208            json!({"id": 12, "name": "Alice"})
209        );
210    }
211    #[derive(Debug, Default, Clone)]
212    struct MinimalModel {
213        errors: Errors,
214    }
215
216    impl Attributes for MinimalModel {
217        fn attribute_names() -> &'static [&'static str] {
218            &[]
219        }
220
221        fn read_attribute(&self, _name: &str) -> Option<Value> {
222            None
223        }
224
225        fn write_attribute(&mut self, name: &str, _value: Value) -> Result<(), AttributeError> {
226            Err(AttributeError::UnknownAttribute(name.to_string()))
227        }
228
229        fn attributes(&self) -> HashMap<String, Value> {
230            HashMap::new()
231        }
232    }
233
234    impl ModelNaming for MinimalModel {}
235    impl Conversion for MinimalModel {}
236
237    impl Model for MinimalModel {
238        fn new() -> Self {
239            Self {
240                errors: Errors::new(),
241            }
242        }
243
244        fn errors(&self) -> &Errors {
245            &self.errors
246        }
247
248        fn errors_mut(&mut self) -> &mut Errors {
249            &mut self.errors
250        }
251    }
252
253    #[test]
254    fn initialize_propagates_attribute_errors() {
255        let result = TestUser::initialize(HashMap::from([(
256            "email".to_string(),
257            json!("alice@example.com"),
258        )]));
259
260        assert!(matches!(
261            result,
262            Err(AttributeError::UnknownAttribute(attribute)) if attribute == "email"
263        ));
264    }
265
266    #[test]
267    fn default_is_valid_returns_true_without_validation_override() {
268        let mut model = MinimalModel::new();
269        model
270            .errors_mut()
271            .add("name", ErrorType::Blank, "can't be blank");
272
273        assert!(model.is_valid());
274        assert!(!model.is_invalid());
275    }
276
277    #[test]
278    fn default_validate_clears_existing_errors_and_returns_true() {
279        let mut model = MinimalModel::new();
280        model
281            .errors_mut()
282            .add("name", ErrorType::Blank, "can't be blank");
283
284        assert!(model.validate());
285        assert!(model.errors().is_empty());
286    }
287
288    #[test]
289    fn custom_model_can_override_validity_checks() {
290        let mut model = TestUser::new();
291        assert!(model.is_valid());
292
293        model.errors.add("name", ErrorType::Blank, "can't be blank");
294        assert!(model.is_invalid());
295    }
296}