Skip to main content

shape/
jsonschema_import.rs

1//! JSON Schema import (draft 2020-12, and the common keywords of earlier
2//! drafts), the inverse of the export: a type becomes a token, bounds become
3//! size builders, formats and patterns their builders, enum and const become
4//! Exact, properties and items become objects and arrays, the compositions
5//! become One, All and Discriminated, and a definition is inlined where it
6//! is referenced, with Define and Refer only where a definition refers to
7//! itself. A property that is not required and has no default is Skip.
8//! Unknown keywords are ignored; an unknown type or reference is an error.
9
10use crate::builders::*;
11use crate::discriminated::discriminated;
12use crate::node::{Node, Token};
13use crate::spec::Spec;
14use crate::value::{Map, Value};
15use regex::Regex;
16use std::collections::HashMap;
17use std::fmt;
18use std::sync::OnceLock;
19
20/// A document that does not import.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct JsonSchemaError(pub String);
23
24impl fmt::Display for JsonSchemaError {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        f.write_str(&self.0)
27    }
28}
29
30impl std::error::Error for JsonSchemaError {}
31
32type Res<T> = Result<T, JsonSchemaError>;
33
34fn fault<T>(msg: impl Into<String>, path: &str) -> Res<T> {
35    let at = if path.is_empty() { "/" } else { path };
36    Err(JsonSchemaError(format!(
37        "JSON Schema: {} at {}",
38        msg.into(),
39        at
40    )))
41}
42
43/// Build a spec from a JSON Schema document. Compile it with `Schema::new`,
44/// or compose it further with the builders.
45pub fn from_json_schema(schema: &Value) -> Res<Spec> {
46    let Value::Obj(m) = schema else {
47        return Err(JsonSchemaError(
48            "JSON Schema: the schema must be an object".to_string(),
49        ));
50    };
51    let defs = match m.get("$defs").or_else(|| m.get("definitions")) {
52        Some(Value::Obj(d)) => d.clone(),
53        _ => Map::new(),
54    };
55    let mut c = Import {
56        root: m,
57        defs,
58        stack: Vec::new(),
59        recursive: HashMap::new(),
60    };
61    c.schema(schema, "")
62}
63
64struct Import<'a> {
65    root: &'a Map,
66    defs: Map,
67    stack: Vec<String>,
68    recursive: HashMap<String, bool>,
69}
70
71fn is_kind_name(name: &str) -> bool {
72    matches!(
73        name,
74        "string" | "number" | "integer" | "boolean" | "null" | "object" | "array"
75    )
76}
77
78fn as_f64(v: Option<&Value>) -> Option<f64> {
79    match v {
80        Some(Value::Num(f)) => Some(*f),
81        _ => None,
82    }
83}
84
85fn as_bool(v: Option<&Value>) -> Option<bool> {
86    match v {
87        Some(Value::Bool(b)) => Some(*b),
88        _ => None,
89    }
90}
91
92fn as_str(v: Option<&Value>) -> Option<&str> {
93    match v {
94        Some(Value::Str(s)) => Some(s.as_str()),
95        _ => None,
96    }
97}
98
99fn empty_object(v: Option<&Value>) -> bool {
100    matches!(v, Some(Value::Obj(m)) if m.is_empty())
101}
102
103/// The export's rendering of Ip: an anyOf of the two address formats.
104fn is_ip_formats(v: Option<&Value>) -> bool {
105    let Some(Value::Arr(any_of)) = v else {
106        return false;
107    };
108    if any_of.len() != 2 {
109        return false;
110    }
111    let only = |i: usize, f: &str| match &any_of[i] {
112        Value::Obj(m) => m.len() == 1 && as_str(m.get("format")) == Some(f),
113        _ => false,
114    };
115    only(0, "ipv4") && only(1, "ipv6")
116}
117
118fn ref_re() -> &'static Regex {
119    static RE: OnceLock<Regex> = OnceLock::new();
120    RE.get_or_init(|| Regex::new(r"^#/(\$defs|definitions)/([^/]+)$").unwrap())
121}
122
123/// The percent-decoding of a reference name.
124fn percent_decode(s: &str) -> String {
125    let bytes = s.as_bytes();
126    let mut out = Vec::with_capacity(bytes.len());
127    let mut i = 0;
128    while i < bytes.len() {
129        if bytes[i] == b'%' && i + 2 < bytes.len() {
130            let hex = &s[i + 1..i + 3];
131            if let Ok(b) = u8::from_str_radix(hex, 16) {
132                out.push(b);
133                i += 3;
134                continue;
135            }
136        }
137        out.push(bytes[i]);
138        i += 1;
139    }
140    String::from_utf8(out).unwrap_or_else(|_| s.to_string())
141}
142
143impl<'a> Import<'a> {
144    fn schema(&mut self, s: &Value, path: &str) -> Res<Spec> {
145        if let Value::Bool(b) = s {
146            return Ok(if *b {
147                Spec::from(Token::Any)
148            } else {
149                Spec::from(never(any()))
150            });
151        }
152        let Value::Obj(m) = s else {
153            return fault("a schema must be an object or boolean", path);
154        };
155        let spec = match as_str(m.get("$ref")) {
156            Some(r) => self.reference(r, path)?,
157            None => self.keywords(m, path)?,
158        };
159        Ok(match as_str(m.get("description")) {
160            Some(d) => Spec::from(describe(d, spec)),
161            None => spec,
162        })
163    }
164
165    /// A definition is inlined at each reference, so validation order
166    /// cannot matter; a definition that refers to itself is defined at its
167    /// outermost expansion and referred to within.
168    fn reference(&mut self, r: &str, path: &str) -> Res<Spec> {
169        let (name, def): (String, Value) = if let Some(m) = ref_re().captures(r) {
170            let name = percent_decode(m.get(2).unwrap().as_str());
171            match self.defs.get(&name) {
172                Some(d) => (name, d.clone()),
173                None => return fault(format!("unknown $ref {:?}", r), path),
174            }
175        } else if r == "#" {
176            (String::new(), Value::Obj(self.root.clone()))
177        } else {
178            return fault(format!("unsupported $ref {:?}", r), path);
179        };
180        let refname = if name.is_empty() {
181            "$root".to_string()
182        } else {
183            name.clone()
184        };
185        if self.stack.contains(&name) {
186            self.recursive.insert(name, true);
187            return Ok(Spec::from(refer(refname, any())));
188        }
189        self.stack.push(name.clone());
190        let was = self.recursive.get(&name).copied().unwrap_or(false);
191        self.recursive.insert(name.clone(), false);
192        let spec = self.schema(&def, path);
193        let recursive = self.recursive.get(&name).copied().unwrap_or(false);
194        self.recursive.insert(name.clone(), was);
195        self.stack.pop();
196        let spec = spec?;
197        Ok(if recursive {
198            Spec::from(define(refname, spec))
199        } else {
200            spec
201        })
202    }
203
204    fn keywords(&mut self, m: &Map, path: &str) -> Res<Spec> {
205        let spec = if let Some(e) = m.get("enum") {
206            match e {
207                Value::Arr(vals) if !vals.is_empty() => Spec::from(exact(vals.clone())),
208                _ => return fault("enum must be a non-empty array", path),
209            }
210        } else if let Some(v) = m.get("const") {
211            Spec::from(exact([v.clone()]))
212        } else if let Some(all_of) = m.get("allOf") {
213            let branches = self.branches(all_of, &format!("{}/allOf", path))?;
214            Spec::from(all(branches))
215        } else if let Some(one_of) = m.get("oneOf") {
216            let sub = format!("{}/oneOf", path);
217            let list = branch_list(one_of, &sub)?;
218            match self.discriminated(list, &sub)? {
219                Some(d) => d,
220                None => Spec::from(one(self.branches(one_of, &sub)?)),
221            }
222        } else if let Some(any_of) = m.get("anyOf").filter(|v| !is_ip_formats(Some(v))) {
223            Spec::from(one(self.branches(any_of, &format!("{}/anyOf", path))?))
224        } else if empty_object(m.get("not")) {
225            Spec::from(never(any()))
226        } else {
227            self.typed(m, path)?
228        };
229        Ok(match m.get("default") {
230            Some(d) => Spec::from(default(d.clone(), spec)),
231            None => spec,
232        })
233    }
234
235    fn branches(&mut self, v: &Value, path: &str) -> Res<Vec<Spec>> {
236        let list = branch_list(v, path)?;
237        let mut out = Vec::with_capacity(list.len());
238        for (i, b) in list.iter().enumerate() {
239            out.push(self.schema(b, &format!("{}/{}", path, i))?);
240        }
241        Ok(out)
242    }
243
244    fn typed(&mut self, m: &Map, path: &str) -> Res<Spec> {
245        let mut types: Vec<Value> = match m.get("type") {
246            Some(Value::Arr(t)) => t.clone(),
247            None => Vec::new(),
248            Some(t) => vec![t.clone()],
249        };
250        let nullable = types.len() > 1 && types.iter().any(|t| as_str(Some(t)) == Some("null"));
251        if nullable {
252            types.retain(|t| as_str(Some(t)) != Some("null"));
253        }
254        for t in &types {
255            match t {
256                Value::Str(name) if is_kind_name(name) => {}
257                other => {
258                    return fault(
259                        format!("unknown type {:?}", crate::stringify::inline_value(other)),
260                        path,
261                    )
262                }
263            }
264        }
265        if types.is_empty() {
266            // No type: the shape the keywords imply, or anything.
267            let has = |k: &str| m.contains_key(k);
268            if has("properties") || has("additionalProperties") || has("required") {
269                types.push(Value::from("object"));
270            } else if has("items") || has("prefixItems") {
271                types.push(Value::from("array"));
272            } else {
273                return self.untyped(m, path);
274            }
275        }
276        let spec = if types.len() == 1 {
277            self.kind(as_str(types.first()).unwrap_or(""), m, path)?
278        } else {
279            let mut kinds = Vec::with_capacity(types.len());
280            for t in &types {
281                kinds.push(self.kind(as_str(Some(t)).unwrap_or(""), m, path)?);
282            }
283            Spec::from(one(kinds))
284        };
285        Ok(if nullable {
286            Spec::from(nullable_(spec))
287        } else {
288            spec
289        })
290    }
291
292    fn kind(&mut self, t: &str, m: &Map, path: &str) -> Res<Spec> {
293        Ok(match t {
294            "string" => import_string(m, path)?,
295            "number" => import_number(Some(Spec::from(Token::Number)), m).unwrap(),
296            "integer" => import_number(Some(Spec::from(Token::Integer)), m).unwrap(),
297            "boolean" => Spec::from(Token::Boolean),
298            "null" => Spec::from(required(crate::spec::null())),
299            "object" => self.object(m, path)?,
300            _ => self.array(m, path)?,
301        })
302    }
303
304    /// Keywords without a type: a pattern or format reads as a string, a
305    /// bound applies to whatever kind the value turns out to be (as a bare
306    /// Min does), and anything else says nothing.
307    fn untyped(&mut self, m: &Map, path: &str) -> Res<Spec> {
308        let format = as_str(m.get("format")).unwrap_or("");
309        if as_str(m.get("pattern")).is_some()
310            || format_builder(format).is_some()
311            || is_ip_formats(m.get("anyOf"))
312        {
313            return import_string(m, path);
314        }
315        // Beside a numeric exclusive bound the length keywords are that bound
316        // written for strings, arrays and objects (Above(1) exports minLength
317        // 2), so only a plain minimum or maximum is a second bound there.
318        let mut view = Map::new();
319        let lo = if as_f64(m.get("exclusiveMinimum")).is_some() {
320            as_f64(m.get("minimum"))
321        } else {
322            first_number(m, &["minimum", "minLength", "minItems", "minProperties"])
323        };
324        if let Some(v) = lo {
325            view.insert("minimum".to_string(), Value::Num(v));
326        }
327        let hi = if as_f64(m.get("exclusiveMaximum")).is_some() {
328            as_f64(m.get("maximum"))
329        } else {
330            first_number(m, &["maximum", "maxLength", "maxItems", "maxProperties"])
331        };
332        if let Some(v) = hi {
333            view.insert("maximum".to_string(), Value::Num(v));
334        }
335        for k in ["exclusiveMinimum", "exclusiveMaximum"] {
336            if let Some(v) = m.get(k) {
337                view.insert(k.to_string(), v.clone());
338            }
339        }
340        // A bare bound rather than one on an Any node, as a user writes.
341        Ok(import_number(None, &view).unwrap_or_else(|| Spec::from(Token::Any)))
342    }
343
344    fn object(&mut self, m: &Map, path: &str) -> Res<Spec> {
345        let props: Map = match m.get("properties") {
346            Some(Value::Obj(p)) => p.clone(),
347            Some(_) => {
348                return fault(
349                    "properties must be an object",
350                    &format!("{}/properties", path),
351                )
352            }
353            None => Map::new(),
354        };
355        let required: Vec<String> = match m.get("required") {
356            Some(Value::Arr(list)) => list
357                .iter()
358                .filter_map(|r| as_str(Some(r)).map(String::from))
359                .collect(),
360            _ => Vec::new(),
361        };
362        let mut obj: Vec<(String, Spec)> = Vec::with_capacity(props.len());
363        for (k, ps) in &props {
364            let spec = self.property(
365                ps,
366                required.contains(k),
367                &format!("{}/properties/{}", path, k),
368            )?;
369            obj.push((k.clone(), spec));
370        }
371        // A required name with no property schema must still be present.
372        for k in &required {
373            if !props.contains_key(k) {
374                obj.push((k.clone(), Spec::from(required_(any()))));
375            }
376        }
377        let ap = m.get("additionalProperties");
378        let spec = match ap {
379            Some(Value::Bool(false)) => {
380                if obj.is_empty() {
381                    Spec::from(closed(Spec::Obj(obj)))
382                } else {
383                    Spec::Obj(obj)
384                }
385            }
386            None | Some(Value::Bool(true)) => Spec::from(open(Spec::Obj(obj))),
387            Some(other) => {
388                let c = self.schema(other, &format!("{}/additionalProperties", path))?;
389                Spec::from(child(c, Spec::Obj(obj)))
390            }
391        };
392        Ok(bounded(spec, m, "minProperties", "maxProperties"))
393    }
394
395    /// A property is required when listed, has its default when given, and
396    /// is otherwise Skip: absent stays absent.
397    fn property(&mut self, ps: &Value, is_required: bool, path: &str) -> Res<Spec> {
398        let spec = self.schema(ps, path)?;
399        if let Value::Obj(m) = ps {
400            if m.contains_key("default") {
401                return Ok(spec);
402            }
403        }
404        Ok(Spec::from(if is_required {
405            required_(spec)
406        } else {
407            skip(spec)
408        }))
409    }
410
411    fn array(&mut self, m: &Map, path: &str) -> Res<Spec> {
412        let items = m.get("items");
413        let spec = if let Some(prefix) = m.get("prefixItems") {
414            let sub = format!("{}/prefixItems", path);
415            let Value::Arr(_) = prefix else {
416                return fault("prefixItems must be an array", &sub);
417            };
418            let elems = self.branches(prefix, &sub)?;
419            // Closed makes a one-element list a tuple rather than an element
420            // shape; items says what may follow (anything, when it is
421            // absent or true).
422            let tuple = closed(Spec::Arr(elems));
423            match items {
424                Some(Value::Bool(false)) => Spec::from(tuple),
425                None | Some(Value::Bool(true)) => Spec::from(rest(Token::Any, tuple)),
426                Some(other) => {
427                    let r = self.schema(other, &format!("{}/items", path))?;
428                    Spec::from(rest(r, tuple))
429                }
430            }
431        } else {
432            match items {
433                None | Some(Value::Bool(true)) => Spec::Arr(Vec::new()),
434                Some(other) => {
435                    let elem = self.schema(other, &format!("{}/items", path))?;
436                    Spec::Arr(vec![elem])
437                }
438            }
439        };
440        Ok(bounded(spec, m, "minItems", "maxItems"))
441    }
442
443    /// A oneOf of objects that each require one property with a distinct
444    /// string const is a discriminated union on that property.
445    fn discriminated(&mut self, branches: &[Value], path: &str) -> Res<Option<Spec>> {
446        if branches.is_empty() {
447            return Ok(None);
448        }
449        let mut objs: Vec<&Map> = Vec::new();
450        let mut props: Vec<&Map> = Vec::new();
451        let mut reqs: Vec<Vec<String>> = Vec::new();
452        for b in branches {
453            let Value::Obj(m) = b else { return Ok(None) };
454            let Some(Value::Obj(p)) = m.get("properties") else {
455                return Ok(None);
456            };
457            let Some(Value::Arr(r)) = m.get("required") else {
458                return Ok(None);
459            };
460            objs.push(m);
461            props.push(p);
462            reqs.push(
463                r.iter()
464                    .filter_map(|k| as_str(Some(k)).map(String::from))
465                    .collect(),
466            );
467        }
468        let const_of = |p: &Map, k: &str| -> Option<String> {
469            match p.get(k) {
470                Some(Value::Obj(pm)) => as_str(pm.get("const")).map(String::from),
471                _ => None,
472            }
473        };
474        // The tag is the first property of the first branch that every
475        // branch declares with a string const and the first requires.
476        let tag = props[0].keys().find(|k| {
477            const_of(props[0], k).is_some()
478                && reqs[0].contains(k)
479                && props.iter().all(|p| const_of(p, k).is_some())
480        });
481        let Some(tag) = tag.cloned() else {
482            return Ok(None);
483        };
484        let mut tags: Vec<String> = Vec::with_capacity(branches.len());
485        for (i, p) in props.iter().enumerate() {
486            let t = const_of(p, &tag).unwrap_or_default();
487            if tags.contains(&t) || !reqs[i].contains(&tag) {
488                return Ok(None);
489            }
490            tags.push(t);
491        }
492        let mut out: Vec<(String, Spec)> = Vec::with_capacity(branches.len());
493        for (i, m) in objs.iter().enumerate() {
494            let mut branch = (*m).clone();
495            let rest_props: Map = props[i]
496                .iter()
497                .filter(|(k, _)| **k != tag)
498                .map(|(k, v)| (k.clone(), v.clone()))
499                .collect();
500            let rest_required: Vec<Value> = reqs[i]
501                .iter()
502                .filter(|k| **k != tag)
503                .map(|k| Value::Str(k.clone()))
504                .collect();
505            branch.insert("properties".to_string(), Value::Obj(rest_props));
506            branch.insert("required".to_string(), Value::Arr(rest_required));
507            let spec = self.object(&branch, &format!("{}/{}", path, i))?;
508            out.push((tags[i].clone(), spec));
509        }
510        Ok(Some(Spec::from(discriminated(tag, out))))
511    }
512}
513
514fn required_(spec: impl Into<Spec>) -> Node {
515    required(spec)
516}
517
518fn nullable_(spec: impl Into<Spec>) -> Node {
519    nullable(spec)
520}
521
522fn branch_list<'v>(v: &'v Value, path: &str) -> Res<&'v [Value]> {
523    match v {
524        Value::Arr(list) => Ok(list),
525        _ => {
526            let word = path.rsplit('/').next().unwrap_or("");
527            fault(format!("{} must be an array", word), path)
528        }
529    }
530}
531
532fn first_number(m: &Map, keys: &[&str]) -> Option<f64> {
533    keys.iter().find_map(|k| as_f64(m.get(*k)))
534}
535
536fn format_builder(format: &str) -> Option<fn(Spec) -> Node> {
537    Some(match format {
538        "email" => |s| email(s),
539        "uri" => |s| url(s),
540        "uuid" => |s| uuid(s),
541        "date-time" => |s| date_time(s),
542        "ipv4" => |s| ipv4(s),
543        "ipv6" => |s| ipv6(s),
544        _ => return None,
545    })
546}
547
548fn import_string(m: &Map, path: &str) -> Res<Spec> {
549    let mut spec = Spec::from(Token::String);
550    let mut plain = true;
551    if let Some(p) = as_str(m.get("pattern")) {
552        if crate::regexp::canonical_regexp(p).is_err() {
553            return fault(format!("bad pattern {:?}", p), path);
554        }
555        spec = Spec::Regexp(p.to_string());
556        plain = false;
557    }
558    let min_length = as_f64(m.get("minLength"));
559    let format = as_str(m.get("format")).unwrap_or("");
560    if let Some(b) = format_builder(format) {
561        spec = Spec::from(b(spec));
562    } else if is_ip_formats(m.get("anyOf")) {
563        spec = Spec::from(ip(spec));
564    } else if plain && !min_length.map(|v| v > 0.0).unwrap_or(false) {
565        // A string with no lower bound is allowed to be empty; a pattern or
566        // format decides for itself.
567        spec = Spec::from(empty(spec));
568    }
569    if let Some(v) = min_length.filter(|v| *v > 1.0) {
570        spec = Spec::from(min(v, spec));
571    }
572    if let Some(v) = as_f64(m.get("maxLength")) {
573        spec = Spec::from(max(v, spec));
574    }
575    Ok(spec)
576}
577
578/// The bounds of a number, on the spec or bare when there is none.
579fn import_number(spec: Option<Spec>, m: &Map) -> Option<Spec> {
580    let mut spec = spec;
581    let base = |s: Option<Spec>| s.unwrap_or_else(|| Spec::from(any()));
582    // A numeric exclusive bound and a plain bound are independent keywords,
583    // so both apply; the boolean form makes the plain bound exclusive.
584    if let Some(v) = as_f64(m.get("exclusiveMinimum")) {
585        spec = Some(Spec::from(above(v, base(spec))));
586    }
587    if let Some(v) = as_f64(m.get("minimum")) {
588        spec = Some(Spec::from(
589            if as_bool(m.get("exclusiveMinimum")) == Some(true) {
590                above(v, base(spec))
591            } else {
592                min(v, base(spec))
593            },
594        ));
595    }
596    if let Some(v) = as_f64(m.get("exclusiveMaximum")) {
597        spec = Some(Spec::from(below(v, base(spec))));
598    }
599    if let Some(v) = as_f64(m.get("maximum")) {
600        spec = Some(Spec::from(
601            if as_bool(m.get("exclusiveMaximum")) == Some(true) {
602                below(v, base(spec))
603            } else {
604                max(v, base(spec))
605            },
606        ));
607    }
608    spec
609}
610
611fn bounded(spec: Spec, m: &Map, lo: &str, hi: &str) -> Spec {
612    let mut spec = spec;
613    if let Some(v) = as_f64(m.get(lo)) {
614        spec = Spec::from(min(v, spec));
615    }
616    if let Some(v) = as_f64(m.get(hi)) {
617        spec = Spec::from(max(v, spec));
618    }
619    spec
620}
621
622#[cfg(test)]
623mod tests {
624    use super::*;
625    use crate::stringify::stringify_node;
626    use crate::Schema;
627    use serde_json::json;
628
629    fn imp(schema: serde_json::Value) -> Result<Spec, JsonSchemaError> {
630        from_json_schema(&Value::from(schema))
631    }
632
633    fn render(schema: serde_json::Value) -> String {
634        stringify_node(&crate::builders::buildize(imp(schema).unwrap()), false)
635    }
636
637    fn fails(schema: serde_json::Value) -> String {
638        imp(schema).unwrap_err().0
639    }
640
641    fn export(schema: serde_json::Value) -> serde_json::Value {
642        let mut v = serde_json::Value::from(Schema::new(imp(schema).unwrap()).json_schema());
643        v.as_object_mut().unwrap().remove("$schema");
644        v
645    }
646
647    #[test]
648    fn imports_kinds_keywords_and_compositions() {
649        assert_eq!(render(json!({"type": "string"})), "String");
650        assert_eq!(
651            render(json!({"type": "string", "minLength": 2, "maxLength": 5})),
652            "String.Min(2).Max(5)"
653        );
654        assert_eq!(render(json!({"type": "string", "minLength": 1})), "String");
655        assert_eq!(render(json!({"type": "string", "pattern": "^a"})), "/^a/");
656        assert_eq!(
657            render(json!({"type": "string", "format": "email"})),
658            "String.Email"
659        );
660        assert_eq!(
661            render(json!({"type": "string", "format": "uri"})),
662            "String.Url"
663        );
664        assert_eq!(
665            render(json!({"type": "string", "format": "uuid"})),
666            "String.Uuid"
667        );
668        assert_eq!(
669            render(json!({"type": "string", "format": "date-time"})),
670            "String.DateTime"
671        );
672        assert_eq!(
673            render(json!({"type": "string", "format": "ipv4"})),
674            "String.Ipv4"
675        );
676        assert_eq!(
677            render(json!({"type": "string", "format": "ipv6"})),
678            "String.Ipv6"
679        );
680        assert_eq!(
681            render(json!({"type": "string", "anyOf": [{"format": "ipv4"}, {"format": "ipv6"}]})),
682            "String.Ip"
683        );
684        assert_eq!(
685            render(json!({"type": "number", "minimum": 1, "maximum": 2})),
686            "Number.Min(1).Max(2)"
687        );
688        assert_eq!(
689            render(json!({"type": "integer", "exclusiveMinimum": 1, "exclusiveMaximum": 3})),
690            "Integer.Above(1).Below(3)"
691        );
692        assert_eq!(
693            render(
694                json!({"type": "number", "minimum": 1, "exclusiveMinimum": true, "maximum": 3, "exclusiveMaximum": true})
695            ),
696            "Number.Above(1).Below(3)"
697        );
698        assert_eq!(render(json!({"type": "boolean"})), "Boolean");
699        assert_eq!(render(json!({"type": "null"})), "null");
700        assert_eq!(render(json!({"type": ["string", "null"]})), "String");
701        assert!(
702            crate::builders::buildize(imp(json!({"type": ["string", "null"]})).unwrap()).nullable
703        );
704        assert_eq!(
705            render(json!({"type": ["string", "number"]})),
706            "One(String,Number)"
707        );
708        assert_eq!(render(json!({"enum": ["a", 1]})), "Exact(a, 1)");
709        assert_eq!(render(json!({"const": 1})), "Exact(1)");
710        assert_eq!(
711            render(json!({"allOf": [{"type": "number"}, {"minimum": 1}]})),
712            "All(Number,Min(1))"
713        );
714        assert_eq!(
715            render(json!({"anyOf": [{"type": "string"}, {"type": "number"}]})),
716            "One(String,Number)"
717        );
718        assert_eq!(
719            render(json!({"oneOf": [{"type": "string"}, {"type": "number"}]})),
720            "One(String,Number)"
721        );
722        assert_eq!(render(json!({"not": {}})), "Never");
723        assert_eq!(render(json!({"not": {"type": "string"}})), "Any");
724        assert_eq!(render(json!({})), "Any");
725        assert_eq!(render(json!({"minLength": 2})), "Min(2)");
726        assert_eq!(
727            render(json!({"maximum": 2, "exclusiveMinimum": 1})),
728            "Above(1).Max(2)"
729        );
730        assert_eq!(render(json!({"exclusiveMaximum": 2})), "Below(2)");
731        assert_eq!(render(json!({"pattern": "^a"})), "/^a/");
732        assert_eq!(render(json!({"format": "email"})), "String.Email");
733        assert_eq!(
734            render(json!({"anyOf": [{"format": "ipv4"}, {"format": "ipv6"}]})),
735            "String.Ip"
736        );
737        assert_eq!(
738            render(json!({"anyOf": [{"format": "ipv4"}]})),
739            "One(String.Ipv4)"
740        );
741        assert_eq!(
742            fails(json!({"anyOf": [{"format": "ipv4"}, 1]})),
743            "JSON Schema: a schema must be an object or boolean at /anyOf/1"
744        );
745        assert_eq!(render(json!({"type": "number", "default": 5})), "5");
746        assert_eq!(
747            render(json!({"type": "string", "description": "d"})),
748            "String"
749        );
750        assert_eq!(
751            render(json!({"properties": {"a": true, "b": false}})),
752            "{a: Any, b: Never}.Open()"
753        );
754        assert_eq!(
755            render(json!({"properties": {"a": {"type": "number"}}})),
756            "{a: 0}.Open()"
757        );
758        assert_eq!(render(json!({"required": ["a"]})), "{a: Any}.Open()");
759        assert_eq!(render(json!({"additionalProperties": false})), "{}");
760        assert_eq!(render(json!({"items": {"type": "number"}})), "[Number]");
761        assert_eq!(
762            render(json!({"prefixItems": [{"type": "number"}]})),
763            "[Number, ...Any]"
764        );
765        assert_eq!(
766            render(json!({"type": "array", "prefixItems": [{"type": "number"}], "items": false})),
767            "[Number]"
768        );
769        assert_eq!(
770            render(
771                json!({"type": "array", "prefixItems": [{"type": "number"}], "items": {"type": "string"}})
772            ),
773            "[Number, ...String]"
774        );
775        assert_eq!(
776            render(json!({"type": "array", "items": true, "minItems": 1, "maxItems": 3})),
777            "[].Min(1).Max(3)"
778        );
779        assert_eq!(
780            render(
781                json!({"type": "object", "additionalProperties": {"type": "number"}, "minProperties": 1, "maxProperties": 2})
782            ),
783            "{}.Child(Number).Min(1).Max(2)"
784        );
785        assert_eq!(
786            render(
787                json!({"type": "object", "properties": {"a": {"type": "number", "default": 1}, "b": {"type": "string"}}, "required": ["b", "c"]})
788            ),
789            "{a: 1, b: String, c: Any}.Open()"
790        );
791        assert_eq!(
792            render(
793                json!({"type": "object", "properties": {"a": {"type": "number"}}, "additionalProperties": false})
794            ),
795            "{a: 0}"
796        );
797        assert_eq!(
798            render(
799                json!({"$defs": {"p": {"type": "number"}}, "properties": {"a": {"$ref": "#/$defs/p"}}})
800            ),
801            "{a: 0}.Open()"
802        );
803        assert_eq!(
804            render(
805                json!({"definitions": {"a b": {"type": "number"}}, "properties": {"a": {"$ref": "#/definitions/a%20b"}}})
806            ),
807            "{a: 0}.Open()"
808        );
809        assert_eq!(
810            render(
811                json!({"$defs": {"n": {"type": "object", "properties": {"kids": {"type": "array", "items": {"$ref": "#/$defs/n"}}}}}, "$ref": "#/$defs/n"})
812            ),
813            "{kids: [Refer(\"n\")]}.Open().Define(\"n\")"
814        );
815        assert_eq!(
816            render(json!({"type": "object", "properties": {"next": {"$ref": "#"}}})),
817            "{next: {next: Refer(\"$root\")}.Open().Define(\"$root\")}.Open()"
818        );
819        assert_eq!(
820            render(json!({"oneOf": [
821                {"type": "object", "properties": {"k": {"const": "a"}, "x": {"type": "number"}}, "required": ["k", "x"]},
822                {"type": "object", "properties": {"k": {"const": "b"}}, "required": ["k"]}
823            ]})),
824            "Discriminated(k,a,b)"
825        );
826        // The export of what is imported reads back the same.
827        let doc = json!({"type": "object", "properties": {"a": {"type": "number", "minimum": 1}, "b": {"type": "string", "minLength": 1}}, "required": ["a"], "additionalProperties": false});
828        assert_eq!(export(doc.clone()), doc);
829    }
830
831    #[test]
832    fn discriminated_needs_a_tag_every_branch_shares() {
833        let plain = |branches: serde_json::Value| render(json!({"oneOf": branches}));
834        assert_eq!(
835            fails(json!({"oneOf": [1, 2]})),
836            "JSON Schema: a schema must be an object or boolean at /oneOf/0"
837        );
838        assert_eq!(
839            plain(json!([{"type": "object"}, {"type": "object"}])),
840            "One({}.Open(),{}.Open())"
841        );
842        assert_eq!(
843            plain(
844                json!([{"properties": {"k": {"const": "a"}}}, {"properties": {"k": {"const": "b"}}}])
845            ),
846            "One({k: Exact(a)}.Open(),{k: Exact(b)}.Open())"
847        );
848        assert_eq!(
849            plain(json!([{"properties": {"k": {"type": "string"}}, "required": ["k"]}])),
850            "One({k: String}.Open())"
851        );
852        assert_eq!(
853            plain(
854                json!([{"properties": {"k": {"const": "a"}}, "required": ["k"]}, {"properties": {"k": {"const": "a"}}, "required": ["k"]}])
855            ),
856            "One({k: Exact(a)}.Open(),{k: Exact(a)}.Open())"
857        );
858        assert_eq!(
859            plain(
860                json!([{"properties": {"k": {"const": "a"}}, "required": ["k"]}, {"properties": {"k": {"const": "b"}}, "required": []}])
861            ),
862            "One({k: Exact(a)}.Open(),{k: Exact(b)}.Open())"
863        );
864        assert_eq!(
865            plain(
866                json!([{"properties": {"k": {"const": "a"}}, "required": ["k"]}, {"properties": {"z": {"const": "b"}}, "required": ["z"]}])
867            ),
868            "One({k: Exact(a)}.Open(),{z: Exact(b)}.Open())"
869        );
870        assert_eq!(plain(json!([])), "One()");
871    }
872
873    #[test]
874    fn rejects_what_it_cannot_read() {
875        assert_eq!(fails(json!(1)), "JSON Schema: the schema must be an object");
876        assert_eq!(
877            fails(json!({"properties": {"a": 1}})),
878            "JSON Schema: a schema must be an object or boolean at /properties/a"
879        );
880        assert_eq!(
881            fails(json!({"$ref": "#/$defs/x"})),
882            "JSON Schema: unknown $ref \"#/$defs/x\" at /"
883        );
884        assert_eq!(
885            fails(json!({"$ref": "http://x"})),
886            "JSON Schema: unsupported $ref \"http://x\" at /"
887        );
888        assert_eq!(
889            fails(json!({"enum": []})),
890            "JSON Schema: enum must be a non-empty array at /"
891        );
892        assert_eq!(
893            fails(json!({"enum": 1})),
894            "JSON Schema: enum must be a non-empty array at /"
895        );
896        assert_eq!(
897            fails(json!({"allOf": 1})),
898            "JSON Schema: allOf must be an array at /allOf"
899        );
900        assert_eq!(
901            fails(json!({"oneOf": 1})),
902            "JSON Schema: oneOf must be an array at /oneOf"
903        );
904        assert_eq!(
905            fails(json!({"anyOf": 1})),
906            "JSON Schema: anyOf must be an array at /anyOf"
907        );
908        assert_eq!(
909            fails(json!({"type": "object", "properties": 1})),
910            "JSON Schema: properties must be an object at /properties"
911        );
912        assert_eq!(
913            fails(json!({"type": "array", "prefixItems": 1})),
914            "JSON Schema: prefixItems must be an array at /prefixItems"
915        );
916        assert_eq!(
917            fails(json!({"type": "nope"})),
918            "JSON Schema: unknown type \"nope\" at /"
919        );
920        assert_eq!(
921            fails(json!({"type": 1})),
922            "JSON Schema: unknown type \"1\" at /"
923        );
924        assert_eq!(
925            fails(json!({"type": "string", "pattern": "["})),
926            "JSON Schema: bad pattern \"[\" at /"
927        );
928        assert_eq!(
929            fails(json!({"type": "object", "properties": {"a": {"type": "x"}}})),
930            "JSON Schema: unknown type \"x\" at /properties/a"
931        );
932        assert_eq!(
933            fails(json!({"type": "array", "items": {"type": "x"}})),
934            "JSON Schema: unknown type \"x\" at /items"
935        );
936        assert_eq!(
937            fails(json!({"type": "array", "prefixItems": [{"type": "x"}]})),
938            "JSON Schema: unknown type \"x\" at /prefixItems/0"
939        );
940        assert_eq!(
941            fails(json!({"type": "array", "prefixItems": [true], "items": {"type": "x"}})),
942            "JSON Schema: unknown type \"x\" at /items"
943        );
944        assert_eq!(
945            fails(json!({"type": "object", "additionalProperties": {"type": "x"}})),
946            "JSON Schema: unknown type \"x\" at /additionalProperties"
947        );
948        assert_eq!(
949            fails(json!({"$defs": {"p": {"type": "x"}}, "$ref": "#/$defs/p"})),
950            "JSON Schema: unknown type \"x\" at /"
951        );
952        assert_eq!(
953            fails(
954                json!({"oneOf": [{"properties": {"k": {"const": "a"}, "x": {"type": "x"}}, "required": ["k"]}, {"properties": {"k": {"const": "b"}}, "required": ["k"]}]})
955            ),
956            "JSON Schema: unknown type \"x\" at /oneOf/0/properties/x"
957        );
958        assert_eq!(
959            fails(json!({"type": ["string", "x"]})),
960            "JSON Schema: unknown type \"x\" at /"
961        );
962        assert_eq!(format!("{}", JsonSchemaError("e".into())), "e");
963        assert_eq!(percent_decode("a%2"), "a%2");
964        assert_eq!(percent_decode("a%zzb"), "a%zzb");
965    }
966}