Skip to main content

robinpath_modules/modules/
schema_mod.rs

1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4    // schema.validate data schema → {valid, errors}
5    rp.register_builtin("schema.validate", |args, _| {
6        let data = args.first().cloned().unwrap_or(Value::Null);
7        let schema = args.get(1).cloned().unwrap_or(Value::Null);
8        let errors = validate_value(&data, &schema, "");
9        let mut obj = indexmap::IndexMap::new();
10        obj.insert("valid".to_string(), Value::Bool(errors.is_empty()));
11        obj.insert("errors".to_string(), Value::Array(
12            errors.iter().map(|e| Value::String(e.clone())).collect()
13        ));
14        Ok(Value::Object(obj))
15    });
16
17    // schema.isValid data schema → bool
18    rp.register_builtin("schema.isValid", |args, _| {
19        let data = args.first().cloned().unwrap_or(Value::Null);
20        let schema = args.get(1).cloned().unwrap_or(Value::Null);
21        let errors = validate_value(&data, &schema, "");
22        Ok(Value::Bool(errors.is_empty()))
23    });
24
25    // schema.getErrors data schema → array of error strings
26    rp.register_builtin("schema.getErrors", |args, _| {
27        let data = args.first().cloned().unwrap_or(Value::Null);
28        let schema = args.get(1).cloned().unwrap_or(Value::Null);
29        let errors = validate_value(&data, &schema, "");
30        Ok(Value::Array(errors.iter().map(|e| Value::String(e.clone())).collect()))
31    });
32
33    // schema.string options? → schema object
34    rp.register_builtin("schema.string", |args, _| {
35        let opts = args.first().cloned().unwrap_or(Value::Null);
36        let mut obj = indexmap::IndexMap::new();
37        obj.insert("type".to_string(), Value::String("string".to_string()));
38        if let Value::Object(o) = opts {
39            for (k, v) in o { obj.insert(k, v); }
40        }
41        Ok(Value::Object(obj))
42    });
43
44    // schema.number options? → schema object
45    rp.register_builtin("schema.number", |args, _| {
46        let opts = args.first().cloned().unwrap_or(Value::Null);
47        let mut obj = indexmap::IndexMap::new();
48        obj.insert("type".to_string(), Value::String("number".to_string()));
49        if let Value::Object(o) = opts {
50            for (k, v) in o { obj.insert(k, v); }
51        }
52        Ok(Value::Object(obj))
53    });
54
55    // schema.boolean → schema object
56    rp.register_builtin("schema.boolean", |_args, _| {
57        let mut obj = indexmap::IndexMap::new();
58        obj.insert("type".to_string(), Value::String("boolean".to_string()));
59        Ok(Value::Object(obj))
60    });
61
62    // schema.array options? → schema object
63    rp.register_builtin("schema.array", |args, _| {
64        let opts = args.first().cloned().unwrap_or(Value::Null);
65        let mut obj = indexmap::IndexMap::new();
66        obj.insert("type".to_string(), Value::String("array".to_string()));
67        if let Value::Object(o) = opts {
68            for (k, v) in o { obj.insert(k, v); }
69        }
70        Ok(Value::Object(obj))
71    });
72
73    // schema.object options? → schema object
74    rp.register_builtin("schema.object", |args, _| {
75        let opts = args.first().cloned().unwrap_or(Value::Null);
76        let mut obj = indexmap::IndexMap::new();
77        obj.insert("type".to_string(), Value::String("object".to_string()));
78        if let Value::Object(o) = opts {
79            for (k, v) in o { obj.insert(k, v); }
80        }
81        Ok(Value::Object(obj))
82    });
83
84    // schema.nullable schema → modified schema
85    rp.register_builtin("schema.nullable", |args, _| {
86        let mut schema = args.first().cloned().unwrap_or(Value::Null);
87        if let Value::Object(ref mut obj) = schema {
88            obj.insert("nullable".to_string(), Value::Bool(true));
89        }
90        Ok(schema)
91    });
92}
93
94fn validate_value(data: &Value, schema: &Value, path: &str) -> Vec<String> {
95    let mut errors = Vec::new();
96    let prefix = if path.is_empty() { "value".to_string() } else { path.to_string() };
97
98    if let Value::Object(schema_obj) = schema {
99        // Check nullable
100        if matches!(data, Value::Null) {
101            if let Some(Value::Bool(true)) = schema_obj.get("nullable") {
102                return errors;
103            }
104        }
105
106        // Check required
107        if let Some(Value::Bool(true)) = schema_obj.get("required") {
108            if matches!(data, Value::Null) {
109                errors.push(format!("{} is required", prefix));
110                return errors;
111            }
112        }
113
114        // Check type
115        if let Some(Value::String(expected_type)) = schema_obj.get("type") {
116            let actual_type = match data {
117                Value::String(_) => "string",
118                Value::Number(_) => "number",
119                Value::Bool(_) => "boolean",
120                Value::Array(_) => "array",
121                Value::Object(_) => "object",
122                Value::Null => "null",
123                _ => "unknown",
124            };
125            if actual_type != expected_type.as_str() && !matches!(data, Value::Null) {
126                errors.push(format!("{} expected type {}, got {}", prefix, expected_type, actual_type));
127                return errors;
128            }
129        }
130
131        // String validations
132        if let Value::String(s) = data {
133            if let Some(Value::Number(n)) = schema_obj.get("minLength") {
134                if (s.len() as f64) < *n {
135                    errors.push(format!("{} must be at least {} chars", prefix, *n as u64));
136                }
137            }
138            if let Some(Value::Number(n)) = schema_obj.get("maxLength") {
139                if s.len() as f64 > *n {
140                    errors.push(format!("{} must be at most {} chars", prefix, *n as u64));
141                }
142            }
143            if let Some(Value::String(pat)) = schema_obj.get("pattern") {
144                if let Ok(re) = regex::Regex::new(pat) {
145                    if !re.is_match(s) {
146                        errors.push(format!("{} does not match pattern {}", prefix, pat));
147                    }
148                }
149            }
150            if let Some(Value::Array(enum_vals)) = schema_obj.get("enum") {
151                if !enum_vals.iter().any(|v| v.to_display_string() == *s) {
152                    errors.push(format!("{} must be one of {:?}", prefix,
153                        enum_vals.iter().map(|v| v.to_display_string()).collect::<Vec<_>>()));
154                }
155            }
156        }
157
158        // Number validations
159        if let Value::Number(n) = data {
160            if let Some(Value::Number(min)) = schema_obj.get("min") {
161                if n < min {
162                    errors.push(format!("{} must be >= {}", prefix, min));
163                }
164            }
165            if let Some(Value::Number(max)) = schema_obj.get("max") {
166                if n > max {
167                    errors.push(format!("{} must be <= {}", prefix, max));
168                }
169            }
170        }
171
172        // Array validations
173        if let Value::Array(arr) = data {
174            if let Some(Value::Number(n)) = schema_obj.get("minItems") {
175                if (arr.len() as f64) < *n {
176                    errors.push(format!("{} must have at least {} items", prefix, *n as u64));
177                }
178            }
179            if let Some(Value::Number(n)) = schema_obj.get("maxItems") {
180                if arr.len() as f64 > *n {
181                    errors.push(format!("{} must have at most {} items", prefix, *n as u64));
182                }
183            }
184            if let Some(item_schema) = schema_obj.get("items") {
185                for (i, item) in arr.iter().enumerate() {
186                    let item_path = format!("{}[{}]", prefix, i);
187                    errors.extend(validate_value(item, item_schema, &item_path));
188                }
189            }
190        }
191
192        // Object property validations
193        if let Value::Object(obj) = data {
194            if let Some(Value::Object(props)) = schema_obj.get("properties") {
195                for (key, prop_schema) in props {
196                    let val = obj.get(key).cloned().unwrap_or(Value::Null);
197                    let prop_path = if prefix.is_empty() { key.clone() } else { format!("{}.{}", prefix, key) };
198                    errors.extend(validate_value(&val, prop_schema, &prop_path));
199                }
200            }
201        }
202    }
203
204    errors
205}