1use serde_json::{Map, Value as JsonValue, json};
15
16use crate::shape::{Field, JsonType, Schema, Shape};
17
18const DIALECT: &str = "https://json-schema.org/draft/2020-12/schema";
19
20impl Schema {
21 pub fn to_json_schema(&self) -> JsonValue {
33 let mut doc = export(&self.root);
34 if let JsonValue::Object(map) = &mut doc {
35 let mut out = Map::new();
36 out.insert("$schema".into(), DIALECT.into());
37 out.extend(std::mem::take(map));
38 if self.samples > 0 {
39 out.insert("x-jlens-samples".into(), self.samples.into());
40 }
41 *map = out;
42 }
43 doc
44 }
45
46 pub fn from_json_schema(doc: &JsonValue) -> Result<Schema, String> {
51 if !doc.is_object() && !doc.is_boolean() {
52 return Err("a JSON Schema must be an object or a boolean".into());
53 }
54 let root = import(doc, doc, 0)?;
55 let samples = doc
56 .get("x-jlens-samples")
57 .and_then(JsonValue::as_u64)
58 .unwrap_or(0);
59 Ok(Schema { root, samples })
60 }
61}
62
63fn export(shape: &Shape) -> JsonValue {
64 let mut out = Map::new();
65 let types: Vec<JsonValue> = shape.types.iter().map(|t| t.name().into()).collect();
66 match types.len() {
67 0 => {}
68 1 => {
69 out.insert("type".into(), types[0].clone());
70 }
71 _ => {
72 out.insert("type".into(), types.into());
73 }
74 }
75 if let Some(label) = &shape.label {
76 out.insert("title".into(), label.clone().into());
77 }
78 if let Some(d) = &shape.description {
79 out.insert("description".into(), d.clone().into());
80 }
81 if let Some(f) = &shape.format {
82 out.insert("format".into(), f.clone().into());
83 }
84 if let Some(e) = &shape.enumeration {
85 out.insert("enum".into(), e.clone().into());
86 }
87 if let Some(m) = shape.minimum {
88 out.insert("minimum".into(), json_number(m));
89 }
90 if let Some(m) = shape.maximum {
91 out.insert("maximum".into(), json_number(m));
92 }
93 if !shape.examples.is_empty() {
94 out.insert("examples".into(), shape.examples.clone().into());
95 }
96 if !shape.fields.is_empty() {
97 let mut props = Map::new();
98 let mut required = Vec::new();
99 for (name, field) in &shape.fields {
100 let mut p = export(&field.shape);
101 if field.required {
102 required.push(JsonValue::from(name.clone()));
103 } else if let JsonValue::Object(m) = &mut p
104 && field.presence > 0.0
105 {
106 m.insert("x-jlens-presence".into(), json_number(field.presence));
107 }
108 props.insert(name.clone(), p);
109 }
110 out.insert("properties".into(), props.into());
111 if !required.is_empty() {
112 out.insert("required".into(), required.into());
113 }
114 }
115 if let Some(items) = &shape.items {
116 out.insert("items".into(), export(items));
117 }
118 if shape.hidden {
119 out.insert("x-jlens-hidden".into(), true.into());
120 }
121 out.into()
122}
123
124fn json_number(n: f64) -> JsonValue {
125 if n.fract() == 0.0 && n.abs() < 9e15 {
126 (n as i64).into()
127 } else {
128 json!(n)
129 }
130}
131
132fn resolve<'a>(root: &'a JsonValue, reference: &str) -> Option<&'a JsonValue> {
133 let pointer = reference.strip_prefix('#')?;
134 root.pointer(pointer)
135}
136
137fn import(node: &JsonValue, root: &JsonValue, depth: usize) -> Result<Shape, String> {
138 if depth > 64 {
139 return Err("$ref nesting is too deep (recursive schema?)".into());
140 }
141 let JsonValue::Object(map) = node else {
142 return Ok(Shape::default()); };
144 let mut shape = match map.get("$ref").and_then(JsonValue::as_str) {
145 Some(r) => {
146 let target = resolve(root, r).ok_or_else(|| format!("cannot resolve $ref {r}"))?;
147 import(target, root, depth + 1)?
148 }
149 None => Shape::default(),
150 };
151 for key in ["allOf", "anyOf", "oneOf"] {
152 if let Some(JsonValue::Array(branches)) = map.get(key) {
153 for branch in branches {
154 let b = import(branch, root, depth + 1)?;
155 merge(&mut shape, b);
156 }
157 }
158 }
159 match map.get("type") {
160 Some(JsonValue::String(t)) => add_type(&mut shape, t),
161 Some(JsonValue::Array(ts)) => {
162 for t in ts.iter().filter_map(JsonValue::as_str) {
163 add_type(&mut shape, t);
164 }
165 }
166 _ => {}
167 }
168 if let Some(JsonValue::Object(props)) = map.get("properties") {
169 let required: Vec<&str> = map
170 .get("required")
171 .and_then(JsonValue::as_array)
172 .map(|r| r.iter().filter_map(JsonValue::as_str).collect())
173 .unwrap_or_default();
174 for (name, p) in props {
175 let field_shape = import(p, root, depth + 1)?;
176 let is_required = required.contains(&name.as_str());
177 let presence = p
178 .get("x-jlens-presence")
179 .and_then(JsonValue::as_f64)
180 .unwrap_or(if is_required { 1.0 } else { 0.0 });
181 shape.fields.insert(
182 name.clone(),
183 Field {
184 shape: field_shape,
185 required: is_required,
186 presence,
187 },
188 );
189 }
190 add_type(&mut shape, "object");
191 }
192 match map.get("items") {
193 Some(items @ JsonValue::Object(_)) => {
194 shape.items = Some(Box::new(import(items, root, depth + 1)?));
195 add_type(&mut shape, "array");
196 }
197 Some(JsonValue::Array(tuple)) => {
198 let mut merged = Shape::default();
200 for item in tuple {
201 merge(&mut merged, import(item, root, depth + 1)?);
202 }
203 shape.items = Some(Box::new(merged));
204 add_type(&mut shape, "array");
205 }
206 _ => {}
207 }
208 if let Some(JsonValue::Array(values)) = map.get("enum") {
209 shape.enumeration = Some(values.clone());
210 }
211 if let Some(value) = map.get("const") {
212 shape.enumeration = Some(vec![value.clone()]);
213 }
214 if let Some(f) = map.get("format").and_then(JsonValue::as_str) {
215 shape.format = Some(f.to_owned());
216 }
217 shape.minimum = map
218 .get("minimum")
219 .and_then(JsonValue::as_f64)
220 .or(shape.minimum);
221 shape.maximum = map
222 .get("maximum")
223 .and_then(JsonValue::as_f64)
224 .or(shape.maximum);
225 if let Some(t) = map.get("title").and_then(JsonValue::as_str) {
226 shape.label = Some(t.to_owned());
227 }
228 if let Some(d) = map.get("description").and_then(JsonValue::as_str) {
229 shape.description = Some(d.to_owned());
230 }
231 if let Some(JsonValue::Array(examples)) = map.get("examples") {
232 shape.examples = examples.clone();
233 }
234 if map.get("x-jlens-hidden").and_then(JsonValue::as_bool) == Some(true) {
235 shape.hidden = true;
236 }
237 Ok(shape)
238}
239
240fn add_type(shape: &mut Shape, name: &str) {
241 if let Some(t) = JsonType::from_name(name)
242 && !shape.types.contains(&t)
243 {
244 shape.types.push(t);
245 shape.types.sort();
246 }
247}
248
249fn merge(shape: &mut Shape, other: Shape) {
251 for t in other.types {
252 if !shape.types.contains(&t) {
253 shape.types.push(t);
254 }
255 }
256 shape.types.sort();
257 for (name, field) in other.fields {
258 match shape.fields.get_mut(&name) {
259 Some(existing) => {
260 existing.required &= field.required;
261 merge(&mut existing.shape, field.shape);
262 }
263 None => {
264 shape.fields.insert(name, field);
265 }
266 }
267 }
268 match (&mut shape.items, other.items) {
269 (Some(a), Some(b)) => merge(a, *b),
270 (slot @ None, Some(b)) => *slot = Some(b),
271 _ => {}
272 }
273 shape.enumeration = shape.enumeration.take().or(other.enumeration);
274 shape.format = shape.format.take().or(other.format);
275 shape.description = shape.description.take().or(other.description);
276 shape.label = shape.label.take().or(other.label);
277}
278
279impl Shape {
280 pub fn without_state(&self) -> Shape {
282 let mut copy = Shape {
283 types: self.types.clone(),
284 fields: Default::default(),
285 items: self.items.as_ref().map(|i| Box::new(i.without_state())),
286 minimum: self.minimum,
287 maximum: self.maximum,
288 format: self.format.clone(),
289 enumeration: self.enumeration.clone(),
290 examples: self.examples.clone(),
291 description: self.description.clone(),
292 label: self.label.clone(),
293 hidden: self.hidden,
294 ..Shape::default()
295 };
296 for (k, f) in &self.fields {
297 copy.fields.insert(
298 k.clone(),
299 Field {
300 shape: f.shape.without_state(),
301 required: f.required,
302 presence: f.presence,
303 },
304 );
305 }
306 copy
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn round_trips_through_json_schema() {
316 let mut schema = Schema::infer(&[
317 json!({"id": 1, "kind": "a", "tags": ["x"], "owner": {"name": "Ann"}, "at": "2024-01-01"}),
318 json!({"id": 2.5, "kind": "a", "tags": [], "at": "2024-02-01"}),
319 json!({"id": 3, "kind": "b", "tags": ["y", "y"], "owner": null, "at": "2024-03-01"}),
320 ]);
321 schema.root.fields["kind"].shape.description = Some("Kind of thing".into());
322 let doc = schema.to_json_schema();
323 assert_eq!(doc["$schema"], DIALECT);
324 assert_eq!(doc["required"], json!(["id", "kind", "tags", "at"]));
325 assert_eq!(
326 doc["properties"]["owner"]["x-jlens-presence"],
327 json!(2.0 / 3.0)
328 );
329 let back = Schema::from_json_schema(&doc).unwrap();
330 assert_eq!(back.root, schema.root.without_state());
331 assert_eq!(back.samples, 3);
332 }
333
334 #[test]
335 fn imports_refs_and_combinators() {
336 let doc = json!({
337 "$defs": {"item": {"type": "object", "properties": {"sku": {"type": "string"}}}},
338 "type": "object",
339 "properties": {
340 "items": {"type": "array", "items": {"$ref": "#/$defs/item"}},
341 "status": {"anyOf": [{"const": "open"}, {"type": "null"}]}
342 }
343 });
344 let schema = Schema::from_json_schema(&doc).unwrap();
345 assert!(schema.shape_at("items[].sku").is_some());
346 assert_eq!(schema.shape_at("status").unwrap().type_summary(), "null");
347 assert_eq!(
348 schema.shape_at("status").unwrap().enumeration,
349 Some(vec![json!("open")])
350 );
351 }
352}