1use crate::builders::bound_arg;
9use crate::node::{Kind, ListMode, Node, Validator};
10use crate::value::{Map, Value};
11
12pub const JSON_SCHEMA_DRAFT: &str = "https://json-schema.org/draft/2020-12/schema";
13
14pub fn json_schema(n: &Node) -> Value {
16 let mut defs = Map::new();
17 let body = node_schema(n, &mut defs);
18 let mut out = Map::new();
19 out.insert("$schema".to_string(), Value::from(JSON_SCHEMA_DRAFT));
20 for (k, v) in body {
21 out.insert(k, v);
22 }
23 if !defs.is_empty() {
24 out.insert("$defs".to_string(), Value::Obj(defs));
25 }
26 Value::Obj(out)
27}
28
29fn type_name(k: Kind) -> Option<&'static str> {
30 Some(match k {
31 Kind::String | Kind::Date | Kind::Regexp => "string",
32 Kind::Number | Kind::NaN => "number",
33 Kind::Integer => "integer",
34 Kind::Boolean => "boolean",
35 Kind::Null => "null",
36 Kind::Object => "object",
37 Kind::Array => "array",
38 _ => return None,
39 })
40}
41
42fn format_name(builder: &str) -> Option<&'static str> {
43 Some(match builder {
44 "Email" => "email",
45 "Url" => "uri",
46 "Uuid" => "uuid",
47 "DateTime" => "date-time",
48 "Ipv4" => "ipv4",
49 "Ipv6" => "ipv6",
50 _ => return None,
51 })
52}
53
54fn node_schema(n: &Node, defs: &mut Map) -> Map {
55 let mut s = Map::new();
56
57 if let Some(name) = &n.refer_name {
60 s.insert("$ref".to_string(), Value::from(format!("#/$defs/{}", name)));
61 describe_schema(n, &mut s);
62 return s;
63 }
64
65 if let Some(t) = type_name(n.kind) {
66 s.insert("type".to_string(), Value::from(t));
67 }
68
69 match n.kind {
70 Kind::String => {
71 if !n.empty {
72 s.insert("minLength".to_string(), Value::from(1));
73 }
74 }
75 Kind::Date => {
76 s.insert("format".to_string(), Value::from("date-time"));
77 }
78 Kind::Regexp => {
79 s.insert("pattern".to_string(), Value::from(n.regexp_src.as_str()));
80 }
81 Kind::Never => {
82 s.insert("not".to_string(), Value::Obj(Map::new()));
83 }
84 Kind::Object => object_schema(n, &mut s, defs),
85 Kind::Array => array_schema(n, &mut s, defs),
86 Kind::List => list_schema(n, &mut s, defs),
87 _ => {}
88 }
89
90 check_schema(n, &mut s);
91
92 if n.nullable {
93 if let Some(t) = s.get("type").cloned() {
94 s.insert("type".to_string(), Value::Arr(vec![t, Value::from("null")]));
95 }
96 }
97
98 let d = &n.default;
101 let carries = !n.required
102 && !n.skippable
103 && n.has_default
104 && !d.is_undefined()
105 && !matches!(d, Value::Func(_))
106 && !matches!(d, Value::Num(f) if f.is_nan())
107 && (!d.is_null() || n.kind == Kind::Null);
108 if carries {
109 s.insert("default".to_string(), d.clone());
110 }
111
112 describe_schema(n, &mut s);
113
114 if let Some(name) = &n.define_name {
115 defs.insert(name.clone(), Value::Obj(s.clone()));
116 }
117 s
118}
119
120fn describe_schema(n: &Node, s: &mut Map) {
121 if let Some(Value::Str(d)) = n.meta.get("description") {
122 s.insert("description".to_string(), Value::Str(d.clone()));
123 }
124}
125
126fn is_any_schema(child: &Node) -> bool {
128 child.kind == Kind::Any && child.refer_name.is_none()
129}
130
131fn object_schema(n: &Node, s: &mut Map, defs: &mut Map) {
132 let mut props = Map::new();
133 let mut required: Vec<String> = Vec::new();
134 for (k, cn) in &n.obj_children {
135 props.insert(k.clone(), Value::Obj(node_schema(cn, defs)));
136 if cn.required {
137 required.push(k.clone());
138 }
139 }
140 if !props.is_empty() {
141 s.insert("properties".to_string(), Value::Obj(props));
142 }
143 if !required.is_empty() {
144 required.sort();
145 s.insert(
146 "required".to_string(),
147 Value::Arr(required.into_iter().map(Value::Str).collect()),
148 );
149 }
150 match &n.obj_rest {
151 None => {
152 s.insert("additionalProperties".to_string(), Value::Bool(false));
153 }
154 Some(rest) if !is_any_schema(rest) => {
155 s.insert(
156 "additionalProperties".to_string(),
157 Value::Obj(node_schema(rest, defs)),
158 );
159 }
160 Some(_) => {}
161 }
162}
163
164fn array_schema(n: &Node, s: &mut Map, defs: &mut Map) {
165 let child = n.arr_rest.as_deref().or(n.arr_child.as_deref());
169 let closed = child.is_none();
170 let child = child.filter(|c| !is_any_schema(c));
171 if !n.arr_children.is_empty() {
172 let fixed: Vec<Value> = n
173 .arr_children
174 .iter()
175 .map(|cn| Value::Obj(node_schema(cn, defs)))
176 .collect();
177 s.insert("prefixItems".to_string(), Value::Arr(fixed));
178 if closed {
179 s.insert("items".to_string(), Value::Bool(false));
180 } else if let Some(c) = child {
181 s.insert("items".to_string(), Value::Obj(node_schema(c, defs)));
182 }
183 } else if let Some(c) = child {
184 s.insert("items".to_string(), Value::Obj(node_schema(c, defs)));
185 }
186}
187
188fn list_schema(n: &Node, s: &mut Map, defs: &mut Map) {
189 let mut branches: Vec<Value> = n
190 .list
191 .iter()
192 .map(|bn| Value::Obj(node_schema(bn, defs)))
193 .collect();
194 if let Some(d) = &n.disc {
195 for (i, b) in branches.iter_mut().enumerate() {
196 let Value::Obj(bs) = b else { continue };
197 let mut props = match bs.shift_remove("properties") {
200 Some(Value::Obj(p)) => p,
201 _ => Map::new(),
202 };
203 let mut tag_schema = Map::new();
204 tag_schema.insert("type".to_string(), Value::from("string"));
205 tag_schema.insert("const".to_string(), Value::from(d.tags[i].as_str()));
206 props.insert(d.tag.clone(), Value::Obj(tag_schema));
207 bs.insert("properties".to_string(), Value::Obj(props));
208 let mut required = vec![d.tag.clone()];
209 if let Some(Value::Arr(have)) = bs.get("required") {
210 for k in have.iter().filter_map(|k| k.as_str()) {
211 if k != d.tag {
212 required.push(k.to_string());
213 }
214 }
215 }
216 required.sort();
217 bs.insert(
218 "required".to_string(),
219 Value::Arr(required.into_iter().map(Value::Str).collect()),
220 );
221 }
222 s.insert("oneOf".to_string(), Value::Arr(branches));
223 return;
224 }
225 let key = if n.list_mode == ListMode::All {
226 "allOf"
227 } else {
228 "anyOf"
229 };
230 s.insert(key.to_string(), Value::Arr(branches));
231}
232
233fn size_families(k: Kind) -> &'static [&'static str] {
237 match k {
238 Kind::Number | Kind::NaN | Kind::Integer => &["minimum"],
239 Kind::String => &["minLength"],
240 Kind::Array => &["minItems"],
241 Kind::Object => &["minProperties"],
242 _ => &["minimum", "minLength", "minItems", "minProperties"],
243 }
244}
245
246fn size_max(lo: &str) -> &'static str {
247 match lo {
248 "minimum" => "maximum",
249 "minLength" => "maxLength",
250 "minItems" => "maxItems",
251 _ => "maxProperties",
252 }
253}
254
255fn check_schema(n: &Node, s: &mut Map) {
256 let families = size_families(n.kind);
257 let mut vs: Vec<&Validator> = n.befores.iter().chain(n.afters.iter()).collect();
258 let mut i = 0;
259 while i < vs.len() {
260 let v = vs[i];
261 i += 1;
262 if let Some(inner) = &v.inner {
264 vs.extend(inner.befores.iter().chain(inner.afters.iter()));
265 continue;
266 }
267 match v.name.as_str() {
268 "Exact" => {
269 s.insert("enum".to_string(), Value::Arr(v.args.clone()));
270 }
271 "Ip" => {
272 let fmt = |f: &str| {
273 let mut m = Map::new();
274 m.insert("format".to_string(), Value::from(f));
275 Value::Obj(m)
276 };
277 s.insert(
278 "anyOf".to_string(),
279 Value::Arr(vec![fmt("ipv4"), fmt("ipv6")]),
280 );
281 }
282 "Min" | "Max" | "Above" | "Below" | "Len" => {
283 let size = v.args.first().and_then(bound_arg).unwrap_or(0.0);
284 for lo in families {
285 let hi = size_max(lo);
286 let numeric = *lo == "minimum";
287 match v.name.as_str() {
288 "Min" => {
289 s.insert(lo.to_string(), Value::Num(size));
290 }
291 "Max" => {
292 s.insert(hi.to_string(), Value::Num(size));
293 }
294 "Above" => {
295 if numeric {
296 s.insert("exclusiveMinimum".to_string(), Value::Num(size));
297 } else {
298 s.insert(lo.to_string(), Value::Num(size + 1.0));
299 }
300 }
301 "Below" => {
302 if numeric {
303 s.insert("exclusiveMaximum".to_string(), Value::Num(size));
304 } else {
305 s.insert(hi.to_string(), Value::Num(size - 1.0));
306 }
307 }
308 _ => {
309 s.insert(lo.to_string(), Value::Num(size));
310 s.insert(hi.to_string(), Value::Num(size));
311 }
312 }
313 }
314 }
315 name => {
316 if let Some(f) = format_name(name) {
317 s.insert("format".to_string(), Value::from(f));
318 } else if name.len() >= 2 && name.starts_with('/') && name.ends_with('/') {
319 s.insert("pattern".to_string(), Value::from(&name[1..name.len() - 1]));
320 }
321 }
322 }
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use crate::builders::*;
330 use crate::discriminated::discriminated;
331 use crate::node::Token;
332 use crate::spec::{arr, obj, Spec};
333 use crate::Schema;
334 use regex::Regex;
335 use serde_json::json;
336
337 fn js(spec: impl Into<Spec>) -> serde_json::Value {
338 let mut v = serde_json::Value::from(Schema::new(spec).json_schema());
339 v.as_object_mut().unwrap().remove("$schema");
340 v
341 }
342
343 #[test]
344 fn renders_kinds_and_containers() {
345 assert_eq!(
346 serde_json::Value::from(Schema::new(Token::String).json_schema()),
347 json!({"$schema": JSON_SCHEMA_DRAFT, "type": "string", "minLength": 1})
348 );
349 assert_eq!(js(empty(Token::String)), json!({"type": "string"}));
350 assert_eq!(
351 js("x"),
352 json!({"type": "string", "minLength": 1, "default": "x"})
353 );
354 assert_eq!(
355 js(Token::Date),
356 json!({"type": "string", "format": "date-time"})
357 );
358 assert_eq!(
359 js(Regex::new("^a").unwrap()),
360 json!({"type": "string", "pattern": "^a"})
361 );
362 assert_eq!(js(never(any())), json!({"not": {}}));
363 assert_eq!(js(Token::Any), json!({}));
364 assert_eq!(js(Token::Integer), json!({"type": "integer"}));
365 assert_eq!(js(f64::NAN), json!({"type": "number"}));
366 assert_eq!(
367 js(Spec::Value(Value::Null)),
368 json!({"type": "null", "default": null})
369 );
370 assert_eq!(js(Spec::Value(Value::Func(1))), json!({}));
371 assert_eq!(js(Token::Function), json!({}));
372 assert_eq!(
373 js(nullable(Token::Number)),
374 json!({"type": ["number", "null"]})
375 );
376 assert_eq!(js(nullable(any())), json!({}));
377 assert_eq!(
378 js(describe("d", Token::Boolean)),
379 json!({"type": "boolean", "description": "d"})
380 );
381 assert_eq!(
382 js(obj([
383 ("a", Spec::from(Token::Number)),
384 ("b", Spec::from(1))
385 ])),
386 json!({"type": "object", "properties": {"a": {"type": "number"}, "b": {"type": "number", "default": 1}}, "required": ["a"], "additionalProperties": false})
387 );
388 assert_eq!(js(Token::Object), json!({"type": "object"}));
389 assert_eq!(
390 js(optional(Token::Object)),
391 json!({"type": "object", "default": {}})
392 );
393 assert_eq!(
394 js(child(Token::Number, Token::Object)),
395 json!({"type": "object", "additionalProperties": {"type": "number"}})
396 );
397 assert_eq!(js(Token::Array), json!({"type": "array"}));
398 assert_eq!(
399 js(arr([Spec::from(Token::Number)])),
400 json!({"type": "array", "items": {"type": "number"}})
401 );
402 assert_eq!(
403 js(arr([Spec::from(Token::Number), Spec::from("x")])),
404 json!({"type": "array", "prefixItems": [{"type": "number"}, {"type": "string", "minLength": 1, "default": "x"}], "items": false})
405 );
406 assert_eq!(
407 js(rest(
408 Token::String,
409 arr([Spec::from(Token::Number), Spec::from(1)])
410 )),
411 json!({"type": "array", "prefixItems": [{"type": "number"}, {"type": "number", "default": 1}], "items": {"type": "string", "minLength": 1}})
412 );
413 assert_eq!(
414 js(rest(
415 Token::Any,
416 arr([Spec::from(Token::Number), Spec::from(1)])
417 )),
418 json!({"type": "array", "prefixItems": [{"type": "number"}, {"type": "number", "default": 1}]})
419 );
420 assert_eq!(js(arr(Vec::<Spec>::new())), json!({"type": "array"}));
421 }
422
423 #[test]
424 fn renders_checks_and_compositions() {
425 assert_eq!(js(exact(["a", "b"])), json!({"enum": ["a", "b"]}));
426 assert_eq!(
427 js(email(any())),
428 json!({"type": "string", "minLength": 1, "format": "email"})
429 );
430 assert_eq!(
431 js(url(any())),
432 json!({"type": "string", "minLength": 1, "format": "uri"})
433 );
434 assert_eq!(
435 js(uuid(any())),
436 json!({"type": "string", "minLength": 1, "format": "uuid"})
437 );
438 assert_eq!(
439 js(date_time(any())),
440 json!({"type": "string", "minLength": 1, "format": "date-time"})
441 );
442 assert_eq!(
443 js(ipv4(any())),
444 json!({"type": "string", "minLength": 1, "format": "ipv4"})
445 );
446 assert_eq!(
447 js(ipv6(any())),
448 json!({"type": "string", "minLength": 1, "format": "ipv6"})
449 );
450 assert_eq!(
451 js(ip(any())),
452 json!({"type": "string", "minLength": 1, "anyOf": [{"format": "ipv4"}, {"format": "ipv6"}]})
453 );
454 assert_eq!(
455 js(min(2, Token::Number)),
456 json!({"type": "number", "minimum": 2})
457 );
458 assert_eq!(
459 js(max(2, Token::Integer)),
460 json!({"type": "integer", "maximum": 2})
461 );
462 assert_eq!(
463 js(above(2, Token::Number)),
464 json!({"type": "number", "exclusiveMinimum": 2})
465 );
466 assert_eq!(
467 js(below(2, Token::Number)),
468 json!({"type": "number", "exclusiveMaximum": 2})
469 );
470 assert_eq!(
471 js(len(2, Token::Number)),
472 json!({"type": "number", "minimum": 2, "maximum": 2})
473 );
474 assert_eq!(
475 js(above(2, Token::String)),
476 json!({"type": "string", "minLength": 3})
477 );
478 assert_eq!(
479 js(below(2, Token::String)),
480 json!({"type": "string", "minLength": 1, "maxLength": 1})
481 );
482 assert_eq!(
483 js(min(2, Token::Array)),
484 json!({"type": "array", "minItems": 2})
485 );
486 assert_eq!(
487 js(max(2, Token::Object)),
488 json!({"type": "object", "maxProperties": 2})
489 );
490 assert_eq!(
491 js(min(2, any())),
492 json!({"minimum": 2, "minLength": 2, "minItems": 2, "minProperties": 2})
493 );
494 assert_eq!(
495 js(len(2, any())),
496 json!({"minimum": 2, "maximum": 2, "minLength": 2, "maxLength": 2, "minItems": 2, "maxItems": 2, "minProperties": 2, "maxProperties": 2})
497 );
498 assert_eq!(
499 js(above(2, any())),
500 json!({"exclusiveMinimum": 2, "minLength": 3, "minItems": 3, "minProperties": 3})
501 );
502 assert_eq!(
503 js(below(2, any())),
504 json!({"exclusiveMaximum": 2, "maxLength": 1, "maxItems": 1, "maxProperties": 1})
505 );
506 assert_eq!(
507 js(check_re(Regex::new("^a").unwrap(), Token::String)),
508 json!({"type": "string", "minLength": 1, "pattern": "^a"})
509 );
510 assert_eq!(
511 js(check(|_, _| true, Token::String)),
512 json!({"type": "string", "minLength": 1})
513 );
514 assert_eq!(
515 js(catch(0, min(2, Token::Number))),
516 json!({"type": "number", "minimum": 2})
517 );
518 assert_eq!(
519 js(coerce(transform(|v, _| v, max(2, Token::Number)))),
520 json!({"type": "number", "maximum": 2})
521 );
522 assert_eq!(
523 js(one([Spec::from(Token::String), Spec::from(Token::Number)])),
524 json!({"anyOf": [{"type": "string", "minLength": 1}, {"type": "number"}]})
525 );
526 assert_eq!(
527 js(all([Spec::from(Token::Number), Spec::from(min(1, any()))])),
528 json!({"allOf": [{"type": "number"}, {"minimum": 1, "minLength": 1, "minItems": 1, "minProperties": 1}]})
529 );
530 assert_eq!(
531 js(discriminated(
532 "k",
533 [
534 ("a", obj([("x", Spec::from(Token::Number))])),
535 ("b", Spec::from(Token::Number))
536 ]
537 )),
538 json!({"oneOf": [
539 {"type": "object", "properties": {"x": {"type": "number"}, "k": {"type": "string", "const": "a"}}, "required": ["k", "x"], "additionalProperties": false},
540 {"type": "number", "properties": {"k": {"type": "string", "const": "b"}}, "required": ["k"]}
541 ]})
542 );
543 assert_eq!(
544 js(obj([
545 ("a", Spec::from(define("d", Token::Number))),
546 ("b", Spec::from(refer("d", any())))
547 ])),
548 json!({"type": "object", "properties": {"a": {"type": "number"}, "b": {"$ref": "#/$defs/d"}}, "required": ["a"], "additionalProperties": false, "$defs": {"d": {"type": "number"}}})
549 );
550 assert_eq!(
551 js(describe("r", refer("d", any()))),
552 json!({"$ref": "#/$defs/d", "description": "r"})
553 );
554 assert_eq!(
555 js(child(refer("d", any()), Token::Object)),
556 json!({"type": "object", "additionalProperties": {"$ref": "#/$defs/d"}})
557 );
558 assert_eq!(
559 js(arr([Spec::from(refer("d", any()))])),
560 json!({"type": "array", "items": {"$ref": "#/$defs/d"}})
561 );
562 assert_eq!(js(skip(Token::Number)), json!({"type": "number"}));
563 assert_eq!(
565 js(discriminated(
566 "k",
567 [("a", obj([("k", Spec::from(Token::String))]))]
568 )),
569 json!({"oneOf": [{"type": "object", "properties": {"k": {"type": "string", "const": "a"}}, "required": ["k"], "additionalProperties": false}]})
570 );
571 assert_eq!(
572 js(optional(Token::Date)),
573 json!({"type": "string", "format": "date-time"})
574 );
575 assert_eq!(
576 serde_json::Value::from(json_schema(&buildize(Token::Number))),
577 json!({"$schema": JSON_SCHEMA_DRAFT, "type": "number"})
578 );
579 }
580}