Skip to main content

toon_validate_core/
validator.rs

1use crate::{
2    error::{Result, TqError},
3    value::{Table, Value},
4};
5use std::collections::{HashMap, HashSet};
6
7pub struct Validator;
8
9impl Validator {
10    pub fn validate(value: &Value) -> Result<()> {
11        match value {
12            Value::Object(obj) => Self::validate_object(obj),
13            Value::Table(table) => Self::validate_table(table),
14            Value::Array(arr) => {
15                for item in arr {
16                    Self::validate(item)?;
17                }
18                Ok(())
19            }
20            _ => Ok(()),
21        }
22    }
23    
24    fn validate_object(obj: &HashMap<String, Value>) -> Result<()> {
25        for value in obj.values() {
26            Self::validate(value)?;
27        }
28        Ok(())
29    }
30    
31    fn validate_table(table: &Table) -> Result<()> {
32        // Check row count matches declaration
33        if table.rows.len() != table.declared_rows {
34            return Err(TqError::TableRowMismatch {
35                name: table.name.clone(),
36                declared: table.declared_rows,
37                actual: table.rows.len(),
38            });
39        }
40        
41        // Check schema consistency across rows
42        if !table.rows.is_empty() {
43            let mut schemas: Vec<HashSet<String>> = Vec::new();
44            
45            for row in &table.rows {
46                let schema: HashSet<String> = row.keys().cloned().collect();
47                schemas.push(schema);
48            }
49            
50            // Check if all rows have the same set of keys
51            let first_schema = &schemas[0];
52            for (idx, schema) in schemas.iter().enumerate().skip(1) {
53                if schema != first_schema {
54                    let missing: Vec<_> = first_schema.difference(schema).collect();
55                    let extra: Vec<_> = schema.difference(first_schema).collect();
56                    
57                    let mut message = format!("Row {} has different schema. ", idx + 1);
58                    if !missing.is_empty() {
59                        message.push_str(&format!("Missing fields: {:?}. ", missing));
60                    }
61                    if !extra.is_empty() {
62                        message.push_str(&format!("Extra fields: {:?}. ", extra));
63                    }
64                    
65                    return Err(TqError::TableSchemaInconsistent {
66                        name: table.name.clone(),
67                        message,
68                    });
69                }
70            }
71        }
72        
73        // Recursively validate values in rows
74        for row in &table.rows {
75            for value in row.values() {
76                Self::validate(value)?;
77            }
78        }
79        
80        Ok(())
81    }
82    
83    pub fn check_structure(value: &Value) -> Vec<String> {
84        let mut issues = Vec::new();
85        Self::check_structure_recursive(value, "", &mut issues);
86        issues
87    }
88    
89    fn check_structure_recursive(value: &Value, path: &str, issues: &mut Vec<String>) {
90        match value {
91            Value::Object(obj) => {
92                if obj.is_empty() {
93                    let prefix = if path.is_empty() { 
94                        String::new() 
95                    } else { 
96                        format!("{}: ", path) 
97                    };
98                    issues.push(format!("{}Empty object", prefix));
99                }
100                for (key, val) in obj {
101                    let new_path = if path.is_empty() {
102                        key.clone()
103                    } else {
104                        format!("{}.{}", path, key)
105                    };
106                    Self::check_structure_recursive(val, &new_path, issues);
107                }
108            }
109            Value::Array(arr) => {
110                if arr.is_empty() {
111                    issues.push(format!("{}: Empty array", path));
112                }
113                for (idx, val) in arr.iter().enumerate() {
114                    let new_path = format!("{}[{}]", path, idx);
115                    Self::check_structure_recursive(val, &new_path, issues);
116                }
117            }
118            Value::Table(table) => {
119                if table.rows.is_empty() && table.declared_rows > 0 {
120                    issues.push(format!("{}: Table declared with {} rows but is empty", path, table.declared_rows));
121                }
122            }
123            _ => {}
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::value::Table;
132
133    #[test]
134    fn test_validate_primitives() {
135        assert!(Validator::validate(&Value::Null).is_ok());
136        assert!(Validator::validate(&Value::Bool(true)).is_ok());
137        assert!(Validator::validate(&Value::Number(42.0)).is_ok());
138        assert!(Validator::validate(&Value::String("test".to_string())).is_ok());
139    }
140
141    #[test]
142    fn test_validate_array() {
143        let arr = Value::Array(vec![
144            Value::Number(1.0),
145            Value::Number(2.0),
146            Value::String("test".to_string()),
147        ]);
148        assert!(Validator::validate(&arr).is_ok());
149    }
150
151    #[test]
152    fn test_validate_object() {
153        let mut obj = HashMap::new();
154        obj.insert("name".to_string(), Value::String("Alice".to_string()));
155        obj.insert("age".to_string(), Value::Number(30.0));
156        obj.insert("active".to_string(), Value::Bool(true));
157        let value = Value::Object(obj);
158        
159        assert!(Validator::validate(&value).is_ok());
160    }
161
162    #[test]
163    fn test_validate_table_correct_rows() {
164        let mut row1 = HashMap::new();
165        row1.insert("id".to_string(), Value::Number(1.0));
166        row1.insert("name".to_string(), Value::String("Alice".to_string()));
167        
168        let mut row2 = HashMap::new();
169        row2.insert("id".to_string(), Value::Number(2.0));
170        row2.insert("name".to_string(), Value::String("Bob".to_string()));
171        
172        let table = Table {
173            name: "users".to_string(),
174            declared_rows: 2,
175            rows: vec![row1, row2],
176        };
177        
178        assert!(Validator::validate(&Value::Table(table)).is_ok());
179    }
180
181    #[test]
182    fn test_validate_table_row_mismatch() {
183        let mut row1 = HashMap::new();
184        row1.insert("id".to_string(), Value::Number(1.0));
185        
186        let table = Table {
187            name: "users".to_string(),
188            declared_rows: 3, // Declared 3 but only 1 row
189            rows: vec![row1],
190        };
191        
192        let result = Validator::validate(&Value::Table(table));
193        assert!(result.is_err());
194        if let Err(e) = result {
195            let error_msg = e.to_string();
196            assert!(error_msg.contains("declared"));
197            assert!(error_msg.contains("3"));
198            assert!(error_msg.contains("1"));
199        }
200    }
201
202    #[test]
203    fn test_validate_table_schema_inconsistent() {
204        let mut row1 = HashMap::new();
205        row1.insert("id".to_string(), Value::Number(1.0));
206        row1.insert("name".to_string(), Value::String("Alice".to_string()));
207        
208        let mut row2 = HashMap::new();
209        row2.insert("id".to_string(), Value::Number(2.0));
210        row2.insert("email".to_string(), Value::String("bob@example.com".to_string()));
211        // Missing "name" field, has extra "email" field
212        
213        let table = Table {
214            name: "users".to_string(),
215            declared_rows: 2,
216            rows: vec![row1, row2],
217        };
218        
219        let result = Validator::validate(&Value::Table(table));
220        assert!(result.is_err());
221        if let Err(e) = result {
222            let error_msg = e.to_string();
223            assert!(error_msg.contains("different schema"));
224        }
225    }
226
227    #[test]
228    fn test_check_structure_empty() {
229        let obj = Value::Object(HashMap::new());
230        let issues = Validator::check_structure(&obj);
231        assert_eq!(issues.len(), 1);
232        assert!(issues[0].contains("Empty object"));
233        
234        let arr = Value::Array(vec![]);
235        let issues = Validator::check_structure(&arr);
236        assert_eq!(issues.len(), 1);
237        assert!(issues[0].contains("Empty array"));
238    }
239
240    #[test]
241    fn test_check_structure_nested() {
242        let mut inner = HashMap::new();
243        inner.insert("empty".to_string(), Value::Array(vec![]));
244        
245        let mut obj = HashMap::new();
246        obj.insert("nested".to_string(), Value::Object(inner));
247        
248        let value = Value::Object(obj);
249        let issues = Validator::check_structure(&value);
250        
251        assert_eq!(issues.len(), 1);
252        assert!(issues[0].contains("nested.empty"));
253        assert!(issues[0].contains("Empty array"));
254    }
255
256    #[test]
257    fn test_check_structure_table_empty() {
258        let table = Table {
259            name: "users".to_string(),
260            declared_rows: 5,
261            rows: vec![],
262        };
263        
264        let mut obj = HashMap::new();
265        obj.insert("users".to_string(), Value::Table(table));
266        
267        let value = Value::Object(obj);
268        let issues = Validator::check_structure(&value);
269        
270        assert_eq!(issues.len(), 1);
271        assert!(issues[0].contains("declared with 5 rows but is empty"));
272    }
273}