salvor_runtime/
validate.rs1use serde_json::Value;
35
36pub fn validate_against_schema(input: &Value, schema: &Value) -> Result<(), String> {
44 validate_at(input, schema, "$")
45}
46
47fn validate_at(input: &Value, schema: &Value, path: &str) -> Result<(), String> {
50 let Some(schema) = schema.as_object() else {
51 return Ok(());
52 };
53
54 if let Some(expected) = schema.get("type") {
55 let names: Vec<&str> = match expected {
56 Value::String(name) => vec![name.as_str()],
57 Value::Array(names) => names.iter().filter_map(Value::as_str).collect(),
58 _ => Vec::new(),
59 };
60 if !names.is_empty() && !names.iter().any(|name| matches_type(input, name)) {
61 return Err(format!(
62 "{path}: expected type {}, got {}",
63 names.join(" or "),
64 type_name(input)
65 ));
66 }
67 }
68
69 if let Some(allowed) = schema.get("enum").and_then(Value::as_array)
70 && !allowed.iter().any(|candidate| candidate == input)
71 {
72 return Err(format!("{path}: value is not one of the allowed values"));
73 }
74
75 if let Some(object) = input.as_object() {
76 if let Some(required) = schema.get("required").and_then(Value::as_array) {
77 for name in required.iter().filter_map(Value::as_str) {
78 if !object.contains_key(name) {
79 return Err(format!("{path}: missing required property `{name}`"));
80 }
81 }
82 }
83 if let Some(properties) = schema.get("properties").and_then(Value::as_object) {
84 for (name, property_schema) in properties {
85 if let Some(value) = object.get(name) {
86 validate_at(value, property_schema, &format!("{path}.{name}"))?;
87 }
88 }
89 }
90 }
91
92 if let (Some(items), Some(item_schema)) = (input.as_array(), schema.get("items")) {
93 for (index, item) in items.iter().enumerate() {
94 validate_at(item, item_schema, &format!("{path}[{index}]"))?;
95 }
96 }
97
98 Ok(())
99}
100
101fn matches_type(value: &Value, name: &str) -> bool {
103 match name {
104 "object" => value.is_object(),
105 "array" => value.is_array(),
106 "string" => value.is_string(),
107 "number" => value.is_number(),
108 "integer" => value.is_i64() || value.is_u64(),
109 "boolean" => value.is_boolean(),
110 "null" => value.is_null(),
111 _ => true,
112 }
113}
114
115fn type_name(value: &Value) -> &'static str {
117 match value {
118 Value::Null => "null",
119 Value::Bool(_) => "boolean",
120 Value::Number(_) => "number",
121 Value::String(_) => "string",
122 Value::Array(_) => "array",
123 Value::Object(_) => "object",
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use serde_json::json;
131
132 #[test]
135 fn obviously_wrong_shapes_are_rejected() {
136 let schema = json!({
137 "type": "object",
138 "required": ["approved"],
139 "properties": {
140 "approved": {"type": "boolean"},
141 "note": {"type": "string"}
142 }
143 });
144 assert!(validate_against_schema(&json!({"approved": true}), &schema).is_ok());
145 assert!(
146 validate_against_schema(&json!({"approved": false, "note": "no"}), &schema).is_ok()
147 );
148
149 assert!(validate_against_schema(&json!("yes"), &schema).is_err());
150 assert!(validate_against_schema(&json!({}), &schema).is_err());
151 assert!(validate_against_schema(&json!({"approved": "yes"}), &schema).is_err());
152
153 let choice = json!({"enum": ["approve", "reject"]});
154 assert!(validate_against_schema(&json!("approve"), &choice).is_ok());
155 assert!(validate_against_schema(&json!("maybe"), &choice).is_err());
156 }
157
158 #[test]
160 fn arrays_and_type_unions_validate() {
161 let schema = json!({"type": "array", "items": {"type": "integer"}});
162 assert!(validate_against_schema(&json!([1, 2, 3]), &schema).is_ok());
163 assert!(validate_against_schema(&json!([1, "two"]), &schema).is_err());
164
165 let union = json!({"type": ["string", "null"]});
166 assert!(validate_against_schema(&json!("text"), &union).is_ok());
167 assert!(validate_against_schema(&json!(null), &union).is_ok());
168 assert!(validate_against_schema(&json!(3), &union).is_err());
169 }
170
171 #[test]
174 fn unimplemented_keywords_do_not_reject() {
175 let schema = json!({"type": "string", "pattern": "^[0-9]+$", "minLength": 99});
176 assert!(validate_against_schema(&json!("not digits"), &schema).is_ok());
177 assert!(validate_against_schema(&json!({"anything": 1}), &json!(true)).is_ok());
178 }
179}