1use 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#[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
43pub 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
103fn 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
123fn 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 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 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 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 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 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 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 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 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 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 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 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
578fn 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 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 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}