1use crate::value::{Constraints, MetaMap, Value};
5use serde_json::{json, Map, Value as JsonValue};
6
7const SCHEMA_URL: &str = "https://json-schema.org/draft/2020-12/schema";
8
9pub fn value_to_json_value(v: &Value) -> JsonValue {
11 match v {
12 Value::Null => JsonValue::Null,
13 Value::Bool(b) => JsonValue::Bool(*b),
14 Value::Int(n) => JsonValue::Number((*n).into()),
15 Value::Float(f) => serde_json::Number::from_f64(*f)
16 .map(JsonValue::Number)
17 .unwrap_or(JsonValue::Null),
18 Value::String(s) | Value::Secret(s) => JsonValue::String(s.clone()),
19 Value::Array(items) => JsonValue::Array(items.iter().map(value_to_json_value).collect()),
20 Value::Object(map) => {
21 let mut out = Map::new();
22 for (k, val) in map.iter() {
23 out.insert(k.clone(), value_to_json_value(val));
24 }
25 JsonValue::Object(out)
26 }
27 }
28}
29
30pub fn metadata_to_json_schema(metadata: &std::collections::HashMap<String, MetaMap>) -> JsonValue {
35 let mut root = json!({
36 "$schema": SCHEMA_URL,
37 "type": "object",
38 "properties": {},
39 "required": []
40 });
41
42 let mut keys: Vec<&String> = metadata.keys().collect();
43 keys.sort_by_key(|k| (k.matches('.').count(), k.len(), k.as_str()));
44
45 for prefix in keys {
46 let mmap = &metadata[prefix];
47 let segments: Vec<&str> = if prefix.is_empty() {
48 vec![]
49 } else {
50 prefix.split('.').collect()
51 };
52 for (field_key, meta) in mmap.iter() {
53 if let Some(ref c) = meta.constraints {
54 let prop = constraints_to_property(c);
55 insert_constraint_at(&mut root, &segments, field_key, prop, c.required);
56 }
57 }
58 }
59
60 root
61}
62
63fn descend_create<'a>(root: &'a mut JsonValue, path: &[&str]) -> &'a mut JsonValue {
64 let mut cur = root;
65 for seg in path {
66 let o = cur.as_object_mut().expect("schema node");
67 let props = o
68 .get_mut("properties")
69 .expect("properties")
70 .as_object_mut()
71 .expect("properties object");
72 cur = props.entry((*seg).to_string()).or_insert_with(|| {
73 json!({
74 "type": "object",
75 "properties": {},
76 "required": []
77 })
78 });
79 }
80 cur
81}
82
83fn insert_constraint_at(
84 root: &mut JsonValue,
85 path: &[&str],
86 field_key: &str,
87 prop: Map<String, JsonValue>,
88 is_required: bool,
89) {
90 let target = descend_create(root, path);
91 {
92 let obj = target.as_object_mut().expect("target schema");
93 let props = obj
94 .get_mut("properties")
95 .expect("properties")
96 .as_object_mut()
97 .expect("properties map");
98 props.insert(field_key.to_string(), JsonValue::Object(prop));
99 }
100 if is_required {
101 let obj = target.as_object_mut().expect("target schema");
102 let req = obj
103 .get_mut("required")
104 .expect("required")
105 .as_array_mut()
106 .expect("required array");
107 if !req.iter().any(|e| e.as_str() == Some(field_key)) {
108 req.push(JsonValue::String(field_key.to_string()));
109 }
110 }
111}
112
113fn constraints_to_property(c: &Constraints) -> Map<String, JsonValue> {
114 let mut prop = Map::new();
115
116 if let Some(ref t) = c.type_name {
117 match t.as_str() {
118 "int" => {
119 prop.insert("type".into(), json!("integer"));
120 }
121 "float" => {
122 prop.insert("type".into(), json!("number"));
123 }
124 "bool" => {
125 prop.insert("type".into(), json!("boolean"));
126 }
127 "string" => {
128 prop.insert("type".into(), json!("string"));
129 }
130 _ => {}
131 }
132 }
133
134 if c.min.is_some() || c.max.is_some() {
135 let is_string = matches!(c.type_name.as_deref(), Some("string"));
136 if is_string {
137 if let Some(mn) = c.min {
138 if mn.fract() == 0.0 {
139 prop.insert("minLength".into(), json!(mn as u64));
140 }
141 }
142 if let Some(mx) = c.max {
143 if mx.fract() == 0.0 {
144 prop.insert("maxLength".into(), json!(mx as u64));
145 }
146 }
147 } else {
148 if let Some(mn) = c.min {
149 prop.insert("minimum".into(), json!(mn));
150 }
151 if let Some(mx) = c.max {
152 prop.insert("maximum".into(), json!(mx));
153 }
154 }
155 }
156
157 if let Some(ref p) = c.pattern {
158 prop.insert("pattern".into(), json!(p));
159 }
160
161 if let Some(ref ev) = c.enum_values {
162 let mut arr: Vec<JsonValue> = ev.iter().cloned().map(JsonValue::String).collect();
163 if let Some(et) = c.type_name.as_deref() {
164 if et == "int" {
165 arr = ev
166 .iter()
167 .filter_map(|s| s.parse::<i64>().ok())
168 .map(|n| JsonValue::Number(n.into()))
169 .collect();
170 } else if et == "float" {
171 arr = ev
172 .iter()
173 .filter_map(|s| s.parse::<f64>().ok())
174 .filter_map(|f| serde_json::Number::from_f64(f).map(JsonValue::Number))
175 .collect();
176 } else if et == "bool" {
177 arr = ev
178 .iter()
179 .map(|s| JsonValue::Bool(s == "true"))
180 .collect();
181 }
182 }
183 if !arr.is_empty() {
184 prop.insert("enum".into(), JsonValue::Array(arr));
185 }
186 }
187
188 prop
189}
190
191#[cfg(feature = "jsonschema")]
193pub fn validate_with_json_schema(
194 instance: &Value,
195 schema: &JsonValue,
196) -> Result<(), Vec<String>> {
197 validate_serde_json(&value_to_json_value(instance), schema)
198}
199
200#[cfg(feature = "jsonschema")]
202pub fn validate_serde_json(instance: &JsonValue, schema: &JsonValue) -> Result<(), Vec<String>> {
203 let validator = match jsonschema::validator_for(schema) {
204 Ok(v) => v,
205 Err(e) => return Err(vec![format!("invalid JSON Schema: {e}")]),
206 };
207 let mut errs = Vec::new();
208 for e in validator.iter_errors(instance) {
209 errs.push(e.to_string());
210 }
211 if errs.is_empty() {
212 Ok(())
213 } else {
214 Err(errs)
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use crate::value::Meta;
222 use std::collections::HashMap;
223
224 #[test]
225 fn nested_metadata_schema() {
226 let mut metadata: HashMap<String, MetaMap> = HashMap::new();
227 let mut root_map = MetaMap::new();
228 root_map.insert(
229 "name".into(),
230 Meta {
231 markers: vec![],
232 args: vec![],
233 type_hint: None,
234 constraints: Some(Constraints {
235 required: true,
236 type_name: Some("string".into()),
237 min: Some(1.0),
238 max: Some(20.0),
239 ..Default::default()
240 }),
241 },
242 );
243 metadata.insert(String::new(), root_map);
244
245 let sch = metadata_to_json_schema(&metadata);
246 assert_eq!(sch["properties"]["name"]["type"], json!("string"));
247 assert_eq!(sch["properties"]["name"]["minLength"], json!(1));
248 assert_eq!(sch["required"], json!(["name"]));
249 }
250}