greentic_setup/
schema_validation.rs1use std::collections::BTreeSet;
8
9use serde_json::Value;
10
11pub fn validate(value: &Value, schema: &Value) -> Vec<String> {
12 let mut errors = Vec::new();
13 validate_at(value, schema, schema, "$", &mut errors);
14 errors
15}
16
17fn validate_at(value: &Value, schema: &Value, root: &Value, path: &str, errors: &mut Vec<String>) {
18 if let Some(reference) = schema.get("$ref").and_then(Value::as_str) {
19 match resolve_ref(root, reference) {
20 Some(resolved) => validate_at(value, resolved, root, path, errors),
21 None => errors.push(format!("{path}: unresolved schema ref {reference}")),
22 }
23 return;
24 }
25
26 if let Some(one_of) = schema.get("oneOf").and_then(Value::as_array) {
27 let valid = one_of.iter().any(|candidate| {
28 let mut candidate_errors = Vec::new();
29 validate_at(value, candidate, root, path, &mut candidate_errors);
30 candidate_errors.is_empty()
31 });
32 if !valid {
33 errors.push(format!("{path}: value does not match any oneOf schema"));
34 }
35 return;
36 }
37
38 if let Some(expected) = schema.get("const")
39 && value != expected
40 {
41 errors.push(format!("{path}: expected const {expected}"));
42 }
43 if let Some(values) = schema.get("enum").and_then(Value::as_array)
44 && !values.iter().any(|candidate| candidate == value)
45 {
46 errors.push(format!("{path}: value is not in enum"));
47 }
48 if let Some(schema_type) = schema.get("type").and_then(Value::as_str)
49 && !type_matches(value, schema_type)
50 {
51 errors.push(format!("{path}: expected {schema_type}"));
52 return;
53 }
54
55 match value {
56 Value::Object(object) => {
57 if let Some(required) = schema.get("required").and_then(Value::as_array) {
58 for key in required.iter().filter_map(Value::as_str) {
59 if !object.contains_key(key) {
60 errors.push(format!("{path}: missing required property {key}"));
61 }
62 }
63 }
64 if let Some(properties) = schema.get("properties").and_then(Value::as_object) {
65 for (key, property_schema) in properties {
66 if let Some(child) = object.get(key) {
67 validate_at(
68 child,
69 property_schema,
70 root,
71 &format!("{path}.{key}"),
72 errors,
73 );
74 }
75 }
76 }
77 }
78 Value::Array(items) => {
79 if let Some(min_items) = schema.get("minItems").and_then(Value::as_u64)
80 && items.len() < min_items as usize
81 {
82 errors.push(format!("{path}: expected at least {min_items} items"));
83 }
84 if schema
85 .get("uniqueItems")
86 .and_then(Value::as_bool)
87 .unwrap_or(false)
88 {
89 let mut seen = BTreeSet::new();
90 for item in items {
91 if !seen.insert(item.to_string()) {
92 errors.push(format!("{path}: array items must be unique"));
93 break;
94 }
95 }
96 }
97 if let Some(item_schema) = schema.get("items") {
98 for (idx, item) in items.iter().enumerate() {
99 validate_at(item, item_schema, root, &format!("{path}[{idx}]"), errors);
100 }
101 }
102 }
103 Value::String(text) => {
104 if let Some(min_length) = schema.get("minLength").and_then(Value::as_u64)
105 && text.len() < min_length as usize
106 {
107 errors.push(format!("{path}: expected string length >= {min_length}"));
108 }
109 }
110 Value::Number(number) => {
111 if let Some(minimum) = schema.get("minimum").and_then(Value::as_i64)
112 && number.as_i64().is_some_and(|actual| actual < minimum)
113 {
114 errors.push(format!("{path}: expected number >= {minimum}"));
115 }
116 }
117 _ => {}
118 }
119}
120
121fn resolve_ref<'a>(root: &'a Value, reference: &str) -> Option<&'a Value> {
122 let pointer = reference.strip_prefix('#')?;
123 root.pointer(pointer)
124}
125
126fn type_matches(value: &Value, schema_type: &str) -> bool {
127 match schema_type {
128 "object" => value.is_object(),
129 "array" => value.is_array(),
130 "string" => value.is_string(),
131 "integer" => value.as_i64().is_some() || value.as_u64().is_some(),
132 "number" => value.is_number(),
133 "boolean" => value.is_boolean(),
134 "null" => value.is_null(),
135 _ => true,
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use serde_json::json;
143
144 #[test]
145 fn validates_required_types_enum_and_refs() {
146 let schema = json!({
147 "type": "object",
148 "required": ["items"],
149 "properties": {
150 "items": {
151 "type": "array",
152 "minItems": 1,
153 "items": {"$ref": "#/$defs/item"}
154 }
155 },
156 "$defs": {
157 "item": {
158 "type": "object",
159 "required": ["kind"],
160 "properties": {
161 "kind": {"enum": ["a", "b"]}
162 }
163 }
164 }
165 });
166
167 assert!(validate(&json!({"items": [{"kind": "a"}]}), &schema).is_empty());
168 let errors = validate(&json!({"items": [{"kind": "c"}]}), &schema);
169 assert_eq!(errors.len(), 1);
170 assert!(errors[0].contains("enum"));
171 }
172}