dampen_cli/commands/check/
model.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashSet;
3use std::fs;
4use std::path::Path;
5
6/// Definition of a model field.
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub struct ModelField {
9    /// Field name (last part of binding path)
10    pub name: String,
11
12    /// Type hint for display purposes
13    #[serde(default)]
14    pub type_name: String,
15
16    /// If true, field has nested children
17    #[serde(default)]
18    pub is_nested: bool,
19
20    /// Nested field definitions for structs
21    #[serde(default)]
22    pub children: Vec<ModelField>,
23}
24
25/// Registry of model fields for validation.
26#[derive(Debug, Clone, Default)]
27pub struct ModelInfo {
28    fields: HashSet<ModelField>,
29}
30
31impl ModelInfo {
32    /// Creates a new empty model info.
33    pub fn new() -> Self {
34        Self {
35            fields: HashSet::new(),
36        }
37    }
38
39    /// Loads model info from a JSON file.
40    ///
41    /// # Arguments
42    ///
43    /// * `path` - Path to the JSON file containing model field definitions
44    ///
45    /// # Returns
46    ///
47    /// A `Result` containing the model info or an error if loading fails.
48    ///
49    /// # Example JSON format
50    ///
51    /// ```json
52    /// [
53    ///   {
54    ///     "name": "count",
55    ///     "type_name": "i32",
56    ///     "is_nested": false,
57    ///     "children": []
58    ///   },
59    ///   {
60    ///     "name": "user",
61    ///     "type_name": "User",
62    ///     "is_nested": true,
63    ///     "children": [
64    ///       {"name": "name", "type_name": "String", "is_nested": false, "children": []},
65    ///       {"name": "email", "type_name": "String", "is_nested": false, "children": []}
66    ///     ]
67    ///   }
68    /// ]
69    /// ```
70    pub fn load_from_json(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
71        let content = fs::read_to_string(path)?;
72        let fields: Vec<ModelField> = serde_json::from_str(&content)?;
73
74        let mut model = Self::new();
75        for field in fields {
76            model.add_field(field);
77        }
78
79        Ok(model)
80    }
81
82    /// Checks if a field path exists in the model.
83    ///
84    /// # Arguments
85    ///
86    /// * `path` - Slice of field names representing the path (e.g., &["user", "name"])
87    ///
88    /// # Returns
89    ///
90    /// `true` if the field path exists, `false` otherwise.
91    pub fn contains_field(&self, path: &[&str]) -> bool {
92        if path.is_empty() {
93            return false;
94        }
95
96        // Find the top-level field
97        let top_level_name = path[0];
98        let top_level_field = self.fields.iter().find(|f| f.name == top_level_name);
99
100        match top_level_field {
101            None => false,
102            Some(field) => {
103                if path.len() == 1 {
104                    // Just checking the top-level field
105                    true
106                } else {
107                    // Need to check nested fields
108                    Self::contains_nested_field(field, &path[1..])
109                }
110            }
111        }
112    }
113
114    /// Helper function to check nested fields recursively.
115    fn contains_nested_field(field: &ModelField, path: &[&str]) -> bool {
116        if path.is_empty() {
117            return true;
118        }
119
120        // If field is not nested, it can't have children
121        if !field.is_nested {
122            return false;
123        }
124
125        // Find the next field in the path
126        let next_name = path[0];
127        let next_field = field.children.iter().find(|f| f.name == next_name);
128
129        match next_field {
130            None => false,
131            Some(child) => {
132                if path.len() == 1 {
133                    true
134                } else {
135                    Self::contains_nested_field(child, &path[1..])
136                }
137            }
138        }
139    }
140
141    /// Gets all top-level field names.
142    ///
143    /// # Returns
144    ///
145    /// A vector of all top-level field names.
146    pub fn top_level_fields(&self) -> Vec<&str> {
147        self.fields.iter().map(|f| f.name.as_str()).collect()
148    }
149
150    /// Gets all available field paths as strings.
151    ///
152    /// # Returns
153    ///
154    /// A vector of all field paths (e.g., "count", "user.name").
155    pub fn all_field_paths(&self) -> Vec<String> {
156        let mut paths = Vec::new();
157
158        for field in &self.fields {
159            paths.push(field.name.clone());
160            Self::collect_nested_paths(field, &field.name, &mut paths);
161        }
162
163        paths
164    }
165
166    /// Helper function to collect nested field paths recursively.
167    fn collect_nested_paths(field: &ModelField, prefix: &str, paths: &mut Vec<String>) {
168        if field.is_nested {
169            for child in &field.children {
170                let path = format!("{}.{}", prefix, child.name);
171                paths.push(path.clone());
172                Self::collect_nested_paths(child, &path, paths);
173            }
174        }
175    }
176
177    /// Adds a field to the model.
178    pub fn add_field(&mut self, field: ModelField) {
179        self.fields.insert(field);
180    }
181
182    /// Gets the number of top-level fields.
183    pub fn len(&self) -> usize {
184        self.fields.len()
185    }
186
187    /// Checks if the model is empty.
188    pub fn is_empty(&self) -> bool {
189        self.fields.is_empty()
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_empty_model() {
199        let model = ModelInfo::new();
200        assert!(model.is_empty());
201        assert_eq!(model.len(), 0);
202        assert!(!model.contains_field(&["count"]));
203    }
204
205    #[test]
206    fn test_add_simple_field() {
207        let mut model = ModelInfo::new();
208
209        let field = ModelField {
210            name: "count".to_string(),
211            type_name: "i32".to_string(),
212            is_nested: false,
213            children: vec![],
214        };
215
216        model.add_field(field);
217
218        assert!(!model.is_empty());
219        assert_eq!(model.len(), 1);
220        assert!(model.contains_field(&["count"]));
221        assert!(!model.contains_field(&["unknown"]));
222    }
223
224    #[test]
225    fn test_nested_field() {
226        let mut model = ModelInfo::new();
227
228        let field = ModelField {
229            name: "user".to_string(),
230            type_name: "User".to_string(),
231            is_nested: true,
232            children: vec![ModelField {
233                name: "name".to_string(),
234                type_name: "String".to_string(),
235                is_nested: false,
236                children: vec![],
237            }],
238        };
239
240        model.add_field(field);
241
242        assert!(model.contains_field(&["user"]));
243        assert!(model.contains_field(&["user", "name"]));
244        assert!(!model.contains_field(&["user", "email"]));
245    }
246
247    #[test]
248    fn test_top_level_fields() {
249        let mut model = ModelInfo::new();
250
251        model.add_field(ModelField {
252            name: "count".to_string(),
253            type_name: "i32".to_string(),
254            is_nested: false,
255            children: vec![],
256        });
257
258        model.add_field(ModelField {
259            name: "enabled".to_string(),
260            type_name: "bool".to_string(),
261            is_nested: false,
262            children: vec![],
263        });
264
265        let fields = model.top_level_fields();
266        assert_eq!(fields.len(), 2);
267        assert!(fields.contains(&"count"));
268        assert!(fields.contains(&"enabled"));
269    }
270
271    #[test]
272    fn test_all_field_paths() {
273        let mut model = ModelInfo::new();
274
275        model.add_field(ModelField {
276            name: "count".to_string(),
277            type_name: "i32".to_string(),
278            is_nested: false,
279            children: vec![],
280        });
281
282        model.add_field(ModelField {
283            name: "user".to_string(),
284            type_name: "User".to_string(),
285            is_nested: true,
286            children: vec![ModelField {
287                name: "name".to_string(),
288                type_name: "String".to_string(),
289                is_nested: false,
290                children: vec![],
291            }],
292        });
293
294        let paths = model.all_field_paths();
295        assert!(paths.contains(&"count".to_string()));
296        assert!(paths.contains(&"user".to_string()));
297        assert!(paths.contains(&"user.name".to_string()));
298    }
299}