robinpath_modules/modules/
schema_mod.rs1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4 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 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 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 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 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 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 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 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 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 if matches!(data, Value::Null) {
101 if let Some(Value::Bool(true)) = schema_obj.get("nullable") {
102 return errors;
103 }
104 }
105
106 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 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 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 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 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 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}