Skip to main content

aegis_document/
validation.rs

1//! Aegis Document Validation
2//!
3//! Schema validation for documents.
4//!
5//! @version 0.1.0
6//! @author AutomataNexus Development Team
7
8use crate::types::{Document, Value};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12// =============================================================================
13// Schema
14// =============================================================================
15
16/// Schema definition for document validation.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Schema {
19    pub name: String,
20    pub fields: HashMap<String, FieldSchema>,
21    pub required: Vec<String>,
22    pub additional_properties: bool,
23}
24
25impl Schema {
26    /// Create a new schema.
27    pub fn new(name: impl Into<String>) -> Self {
28        Self {
29            name: name.into(),
30            fields: HashMap::new(),
31            required: Vec::new(),
32            additional_properties: true,
33        }
34    }
35
36    /// Add a field to the schema.
37    pub fn field(mut self, name: impl Into<String>, schema: FieldSchema) -> Self {
38        self.fields.insert(name.into(), schema);
39        self
40    }
41
42    /// Add a required field.
43    pub fn require(mut self, name: impl Into<String>) -> Self {
44        self.required.push(name.into());
45        self
46    }
47
48    /// Set whether additional properties are allowed.
49    pub fn additional_properties(mut self, allow: bool) -> Self {
50        self.additional_properties = allow;
51        self
52    }
53
54    /// Validate a document against this schema.
55    pub fn validate(&self, doc: &Document) -> ValidationResult {
56        let mut errors = Vec::new();
57
58        for required in &self.required {
59            if !doc.contains(required) {
60                errors.push(format!("Missing required field: {}", required));
61            }
62        }
63
64        for (field_name, field_schema) in &self.fields {
65            if let Some(value) = doc.get(field_name) {
66                if let Err(err) = field_schema.validate(value) {
67                    errors.push(format!("Field '{}': {}", field_name, err));
68                }
69            }
70        }
71
72        if !self.additional_properties {
73            for key in doc.keys() {
74                if !self.fields.contains_key(key) {
75                    errors.push(format!("Unknown field: {}", key));
76                }
77            }
78        }
79
80        ValidationResult {
81            is_valid: errors.is_empty(),
82            errors,
83        }
84    }
85}
86
87// =============================================================================
88// Field Schema
89// =============================================================================
90
91/// Schema for a single field.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct FieldSchema {
94    pub field_type: FieldType,
95    pub nullable: bool,
96    pub min: Option<f64>,
97    pub max: Option<f64>,
98    pub min_length: Option<usize>,
99    pub max_length: Option<usize>,
100    pub pattern: Option<String>,
101    pub enum_values: Option<Vec<Value>>,
102    pub items: Option<Box<FieldSchema>>,
103    pub properties: Option<HashMap<String, FieldSchema>>,
104}
105
106impl FieldSchema {
107    pub fn new(field_type: FieldType) -> Self {
108        Self {
109            field_type,
110            nullable: false,
111            min: None,
112            max: None,
113            min_length: None,
114            max_length: None,
115            pattern: None,
116            enum_values: None,
117            items: None,
118            properties: None,
119        }
120    }
121
122    pub fn string() -> Self {
123        Self::new(FieldType::String)
124    }
125
126    pub fn int() -> Self {
127        Self::new(FieldType::Int)
128    }
129
130    pub fn float() -> Self {
131        Self::new(FieldType::Float)
132    }
133
134    pub fn bool() -> Self {
135        Self::new(FieldType::Bool)
136    }
137
138    pub fn array(items: FieldSchema) -> Self {
139        let mut schema = Self::new(FieldType::Array);
140        schema.items = Some(Box::new(items));
141        schema
142    }
143
144    pub fn object() -> Self {
145        Self::new(FieldType::Object)
146    }
147
148    pub fn nullable(mut self) -> Self {
149        self.nullable = true;
150        self
151    }
152
153    pub fn min(mut self, min: f64) -> Self {
154        self.min = Some(min);
155        self
156    }
157
158    pub fn max(mut self, max: f64) -> Self {
159        self.max = Some(max);
160        self
161    }
162
163    pub fn min_length(mut self, len: usize) -> Self {
164        self.min_length = Some(len);
165        self
166    }
167
168    pub fn max_length(mut self, len: usize) -> Self {
169        self.max_length = Some(len);
170        self
171    }
172
173    pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
174        self.pattern = Some(pattern.into());
175        self
176    }
177
178    pub fn enum_values(mut self, values: Vec<Value>) -> Self {
179        self.enum_values = Some(values);
180        self
181    }
182
183    /// Validate a value against this field schema.
184    pub fn validate(&self, value: &Value) -> Result<(), String> {
185        if value.is_null() {
186            if self.nullable {
187                return Ok(());
188            }
189            return Err("Value cannot be null".to_string());
190        }
191
192        if !self.field_type.matches(value) {
193            return Err(format!(
194                "Expected type {:?}, got {:?}",
195                self.field_type,
196                value_type(value)
197            ));
198        }
199
200        if let Some(ref enum_values) = self.enum_values {
201            if !enum_values.contains(value) {
202                return Err("Value not in allowed enum values".to_string());
203            }
204        }
205
206        match value {
207            Value::Int(n) => {
208                if let Some(min) = self.min {
209                    if (*n as f64) < min {
210                        return Err(format!("Value {} is less than minimum {}", n, min));
211                    }
212                }
213                if let Some(max) = self.max {
214                    if (*n as f64) > max {
215                        return Err(format!("Value {} is greater than maximum {}", n, max));
216                    }
217                }
218            }
219            Value::Float(f) => {
220                if let Some(min) = self.min {
221                    if *f < min {
222                        return Err(format!("Value {} is less than minimum {}", f, min));
223                    }
224                }
225                if let Some(max) = self.max {
226                    if *f > max {
227                        return Err(format!("Value {} is greater than maximum {}", f, max));
228                    }
229                }
230            }
231            Value::String(s) => {
232                if let Some(min_len) = self.min_length {
233                    if s.len() < min_len {
234                        return Err(format!(
235                            "String length {} is less than minimum {}",
236                            s.len(),
237                            min_len
238                        ));
239                    }
240                }
241                if let Some(max_len) = self.max_length {
242                    if s.len() > max_len {
243                        return Err(format!(
244                            "String length {} is greater than maximum {}",
245                            s.len(),
246                            max_len
247                        ));
248                    }
249                }
250                if let Some(ref pattern) = self.pattern {
251                    let re = regex::Regex::new(pattern)
252                        .map_err(|_| "Invalid regex pattern".to_string())?;
253                    if !re.is_match(s) {
254                        return Err(format!("String does not match pattern: {}", pattern));
255                    }
256                }
257            }
258            Value::Array(arr) => {
259                if let Some(min_len) = self.min_length {
260                    if arr.len() < min_len {
261                        return Err(format!(
262                            "Array length {} is less than minimum {}",
263                            arr.len(),
264                            min_len
265                        ));
266                    }
267                }
268                if let Some(max_len) = self.max_length {
269                    if arr.len() > max_len {
270                        return Err(format!(
271                            "Array length {} is greater than maximum {}",
272                            arr.len(),
273                            max_len
274                        ));
275                    }
276                }
277                if let Some(ref items_schema) = self.items {
278                    for (i, item) in arr.iter().enumerate() {
279                        if let Err(e) = items_schema.validate(item) {
280                            return Err(format!("Array item {}: {}", i, e));
281                        }
282                    }
283                }
284            }
285            Value::Object(obj) => {
286                if let Some(ref props) = self.properties {
287                    for (key, prop_schema) in props {
288                        if let Some(value) = obj.get(key) {
289                            if let Err(e) = prop_schema.validate(value) {
290                                return Err(format!("Property '{}': {}", key, e));
291                            }
292                        }
293                    }
294                }
295            }
296            _ => {}
297        }
298
299        Ok(())
300    }
301}
302
303fn value_type(value: &Value) -> &'static str {
304    match value {
305        Value::Null => "null",
306        Value::Bool(_) => "bool",
307        Value::Int(_) => "int",
308        Value::Float(_) => "float",
309        Value::String(_) => "string",
310        Value::Array(_) => "array",
311        Value::Object(_) => "object",
312    }
313}
314
315// =============================================================================
316// Field Type
317// =============================================================================
318
319/// Type of a field value.
320#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
321pub enum FieldType {
322    String,
323    Int,
324    Float,
325    Number,
326    Bool,
327    Array,
328    Object,
329    Any,
330}
331
332impl FieldType {
333    fn matches(&self, value: &Value) -> bool {
334        match (self, value) {
335            (Self::Any, _) => true,
336            (Self::String, Value::String(_)) => true,
337            (Self::Int, Value::Int(_)) => true,
338            (Self::Float, Value::Float(_)) => true,
339            (Self::Number, Value::Int(_) | Value::Float(_)) => true,
340            (Self::Bool, Value::Bool(_)) => true,
341            (Self::Array, Value::Array(_)) => true,
342            (Self::Object, Value::Object(_)) => true,
343            _ => false,
344        }
345    }
346}
347
348// =============================================================================
349// Validation Result
350// =============================================================================
351
352/// Result of schema validation.
353#[derive(Debug, Clone)]
354pub struct ValidationResult {
355    pub is_valid: bool,
356    pub errors: Vec<String>,
357}
358
359impl ValidationResult {
360    pub fn valid() -> Self {
361        Self {
362            is_valid: true,
363            errors: Vec::new(),
364        }
365    }
366
367    pub fn invalid(errors: Vec<String>) -> Self {
368        Self {
369            is_valid: false,
370            errors,
371        }
372    }
373}
374
375// =============================================================================
376// Schema Builder
377// =============================================================================
378
379/// Builder for creating schemas.
380pub struct SchemaBuilder {
381    schema: Schema,
382}
383
384impl SchemaBuilder {
385    pub fn new(name: impl Into<String>) -> Self {
386        Self {
387            schema: Schema::new(name),
388        }
389    }
390
391    pub fn field(mut self, name: impl Into<String>, schema: FieldSchema) -> Self {
392        self.schema.fields.insert(name.into(), schema);
393        self
394    }
395
396    pub fn required_field(mut self, name: impl Into<String>, schema: FieldSchema) -> Self {
397        let name = name.into();
398        self.schema.fields.insert(name.clone(), schema);
399        self.schema.required.push(name);
400        self
401    }
402
403    pub fn additional_properties(mut self, allow: bool) -> Self {
404        self.schema.additional_properties = allow;
405        self
406    }
407
408    pub fn build(self) -> Schema {
409        self.schema
410    }
411}
412
413// =============================================================================
414// Tests
415// =============================================================================
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420
421    #[test]
422    fn test_type_validation() {
423        let schema = FieldSchema::string();
424        assert!(schema.validate(&Value::String("hello".to_string())).is_ok());
425        assert!(schema.validate(&Value::Int(42)).is_err());
426
427        let schema = FieldSchema::int();
428        assert!(schema.validate(&Value::Int(42)).is_ok());
429        assert!(schema.validate(&Value::String("42".to_string())).is_err());
430    }
431
432    #[test]
433    fn test_nullable() {
434        let schema = FieldSchema::string();
435        assert!(schema.validate(&Value::Null).is_err());
436
437        let schema = FieldSchema::string().nullable();
438        assert!(schema.validate(&Value::Null).is_ok());
439    }
440
441    #[test]
442    fn test_range_validation() {
443        let schema = FieldSchema::int().min(0.0).max(100.0);
444
445        assert!(schema.validate(&Value::Int(50)).is_ok());
446        assert!(schema.validate(&Value::Int(-1)).is_err());
447        assert!(schema.validate(&Value::Int(101)).is_err());
448    }
449
450    #[test]
451    fn test_string_length() {
452        let schema = FieldSchema::string().min_length(3).max_length(10);
453
454        assert!(schema.validate(&Value::String("hello".to_string())).is_ok());
455        assert!(schema.validate(&Value::String("hi".to_string())).is_err());
456        assert!(schema
457            .validate(&Value::String("hello world!".to_string()))
458            .is_err());
459    }
460
461    #[test]
462    fn test_pattern_validation() {
463        let schema = FieldSchema::string().pattern(r"^\d{3}-\d{4}$");
464
465        assert!(schema.validate(&Value::String("123-4567".to_string())).is_ok());
466        assert!(schema.validate(&Value::String("invalid".to_string())).is_err());
467    }
468
469    #[test]
470    fn test_schema_validation() {
471        let schema = SchemaBuilder::new("User")
472            .required_field("name", FieldSchema::string().min_length(1))
473            .required_field("age", FieldSchema::int().min(0.0))
474            .field("email", FieldSchema::string().nullable())
475            .build();
476
477        let mut doc = Document::new();
478        doc.set("name", "Alice");
479        doc.set("age", 30i64);
480
481        let result = schema.validate(&doc);
482        assert!(result.is_valid);
483
484        let mut invalid_doc = Document::new();
485        invalid_doc.set("name", "Bob");
486
487        let result = schema.validate(&invalid_doc);
488        assert!(!result.is_valid);
489        assert!(result.errors.iter().any(|e| e.contains("age")));
490    }
491
492    #[test]
493    fn test_enum_validation() {
494        let schema = FieldSchema::string().enum_values(vec![
495            Value::String("active".to_string()),
496            Value::String("inactive".to_string()),
497        ]);
498
499        assert!(schema.validate(&Value::String("active".to_string())).is_ok());
500        assert!(schema.validate(&Value::String("unknown".to_string())).is_err());
501    }
502
503    #[test]
504    fn test_array_validation() {
505        let schema = FieldSchema::array(FieldSchema::int()).min_length(1).max_length(5);
506
507        assert!(schema.validate(&Value::Array(vec![Value::Int(1), Value::Int(2)])).is_ok());
508        assert!(schema.validate(&Value::Array(vec![])).is_err());
509        assert!(schema
510            .validate(&Value::Array(vec![Value::String("not an int".to_string())]))
511            .is_err());
512    }
513}