1#![doc = include_str!("../README.md")]
2
3mod fmt;
4mod man;
5mod render;
6mod schema;
7mod sections;
8
9use core::fmt::Write;
10
11use jsonschema_schema::{Schema, SchemaValue};
12
13use fmt::{Fmt, format_header, format_type};
14use man::{write_description, write_section};
15use render::{
16 render_additional_properties, render_pattern_properties, render_properties, render_subschema,
17};
18use schema::{get_description, required_set, schema_type_str};
19use sections::{
20 render_definitions_section, render_examples_section, render_schema_section,
21 render_variants_section,
22};
23
24pub use schema::{navigate_pointer, resolve_ref as resolve_schema_ref};
25
26pub struct ExplainError {
28 pub instance_path: String,
30 pub message: String,
32}
33
34pub struct ExplainOptions {
36 pub color: bool,
38 pub syntax_highlight: bool,
40 pub width: usize,
42 pub validation_errors: Vec<ExplainError>,
44 pub extended: bool,
46}
47
48pub fn explain(schema: &SchemaValue, name: &str, opts: &ExplainOptions) -> String {
54 let Some(s) = schema.as_schema() else {
55 let mut out = String::new();
57 let f = Fmt::from_opts(opts);
58 let upper = name.to_uppercase();
59 let header = format_header(&upper, name, opts.width);
60 let _ = writeln!(out, "{}{header}{}\n", f.bold, f.reset);
61 return out;
62 };
63 explain_schema(s, schema, name, opts)
64}
65
66fn explain_schema(s: &Schema, root: &SchemaValue, name: &str, opts: &ExplainOptions) -> String {
68 let mut out = String::new();
69 let f = Fmt::from_opts(opts);
70
71 let s = if f.extended {
74 s.clone()
75 } else {
76 s.absolute().flatten(root)
77 };
78 let render_root = SchemaValue::Schema(Box::new(s.clone()));
79
80 let title = s.title.as_deref();
81 let description = get_description(&s);
82
83 let label = std::path::Path::new(name)
84 .file_name()
85 .and_then(|f| f.to_str())
86 .unwrap_or(name);
87 let center = title.unwrap_or(label);
88 let header = format_header(label, center, opts.width);
89 let _ = writeln!(out, "{}{header}{}\n", f.bold, f.reset);
90
91 if !opts.validation_errors.is_empty() {
92 write_section(&mut out, "VALIDATION ERRORS", &f);
93 for err in &opts.validation_errors {
94 let path = if err.instance_path.is_empty() {
95 "(root)"
96 } else {
97 &err.instance_path
98 };
99 let _ = writeln!(out, " {}{path}{}: {}", f.red, f.reset, err.message);
100 }
101 out.push('\n');
102 }
103
104 if let Some(t) = title {
105 write_section(&mut out, "TITLE", &f);
106 let _ = writeln!(out, " {}{t}{}", f.bold, f.reset);
107 out.push('\n');
108 }
109
110 if let Some(desc) = description {
111 write_section(&mut out, "DESCRIPTION", &f);
112 write_description(&mut out, desc, &f, " ");
113 out.push('\n');
114 }
115
116 if f.extended
117 && let Some(ref comment) = s.comment
118 {
119 write_section(&mut out, "COMMENT", &f);
120 write_description(&mut out, comment, &f, " ");
121 out.push('\n');
122 }
123
124 render_schema_section(&mut out, &s, &f);
125
126 let type_str = schema_type_str(&s);
127 if let Some(ref ty) = type_str {
128 write_section(&mut out, "TYPE", &f);
129 let _ = writeln!(out, " {}", format_type(ty, &f));
130 out.push('\n');
131 }
132
133 let required = required_set(&s);
134 if !s.properties.is_empty() {
135 write_section(&mut out, "PROPERTIES", &f);
136 render_properties(&mut out, &s.properties, &required, &render_root, &f, 1);
137 out.push('\n');
138 }
139
140 render_pattern_properties(&mut out, &s, root, &f, 0, " ");
141 render_additional_properties(&mut out, &s, root, &f, 0, " ");
142
143 if s.if_.is_some() {
145 use crate::schema::variant_summary;
146 write_section(&mut out, "CONDITIONAL", &f);
147 if let Some(ref if_sv) = s.if_ {
148 let summary = variant_summary(if_sv, root, &f);
149 let _ = writeln!(out, " If: {summary}");
150 }
151 if let Some(ref then_sv) = s.then_ {
152 let summary = variant_summary(then_sv, root, &f);
153 let _ = writeln!(out, " Then: {summary}");
154 }
155 if let Some(ref else_sv) = s.else_ {
156 let summary = variant_summary(else_sv, root, &f);
157 let _ = writeln!(out, " Else: {summary}");
158 }
159 out.push('\n');
160 }
161
162 if type_str.as_deref() == Some("array")
163 && let Some(ref items) = s.items
164 {
165 write_section(&mut out, "ITEMS", &f);
166 render_subschema(&mut out, items, &render_root, &f, 1);
167 out.push('\n');
168 }
169
170 render_examples_section(&mut out, &s, &f);
171 render_variants_section(&mut out, &s, root, &f);
174 render_definitions_section(&mut out, &s, &render_root, &f);
175
176 out
177}
178
179pub fn explain_at_path(
188 schema: &SchemaValue,
189 pointer: &str,
190 name: &str,
191 opts: &ExplainOptions,
192) -> Result<String, String> {
193 let sub = navigate_pointer(schema, schema, pointer)?;
194 Ok(explain(sub, name, opts))
195}
196
197#[cfg(test)]
198#[allow(clippy::unwrap_used)]
199mod tests {
200 use super::*;
201 use crate::fmt::{BLUE, BOLD, CYAN, GREEN, RESET, format_header, format_type};
202 use serde_json::json;
203
204 fn sv(val: serde_json::Value) -> SchemaValue {
207 SchemaValue::Schema(Box::new(jsonschema_migrate::migrate(val).unwrap()))
208 }
209
210 fn plain() -> ExplainOptions {
211 ExplainOptions {
212 color: false,
213 syntax_highlight: false,
214 width: 80,
215 validation_errors: vec![],
216 extended: false,
217 }
218 }
219
220 fn colored() -> ExplainOptions {
221 ExplainOptions {
222 color: true,
223 syntax_highlight: true,
224 width: 80,
225 validation_errors: vec![],
226 extended: false,
227 }
228 }
229
230 #[test]
231 fn simple_object_schema() {
232 let schema = sv(json!({
233 "title": "Test",
234 "description": "A test schema",
235 "type": "object",
236 "properties": {
237 "name": {
238 "type": "string",
239 "description": "The name field"
240 },
241 "age": {
242 "type": "integer",
243 "description": "The age field"
244 }
245 }
246 }));
247
248 let output = explain(&schema, "test", &plain());
249 assert!(output.contains("TITLE"));
250 assert!(output.contains("Test"));
251 assert!(!output.contains("Test - A test schema"));
252 assert!(output.contains("DESCRIPTION"));
253 assert!(output.contains("A test schema"));
254 assert!(output.contains("PROPERTIES"));
255 assert!(output.contains("name (string)"));
256 assert!(output.contains("The name field"));
257 assert!(output.contains("age (integer)"));
258 }
259
260 #[test]
261 fn nested_object_renders_with_indentation() {
262 let schema = sv(json!({
263 "type": "object",
264 "properties": {
265 "config": {
266 "type": "object",
267 "description": "Configuration block",
268 "properties": {
269 "debug": {
270 "type": "boolean",
271 "description": "Enable debug mode"
272 }
273 }
274 }
275 }
276 }));
277
278 let output = explain(&schema, "nested", &plain());
279 assert!(output.contains("config (object)"));
280 assert!(output.contains("debug (boolean)"));
281 assert!(output.contains("Enable debug mode"));
282 }
283
284 #[test]
285 fn enum_values_listed() {
286 let schema = sv(json!({
287 "type": "object",
288 "properties": {
289 "level": {
290 "type": "string",
291 "enum": ["low", "medium", "high"]
292 }
293 }
294 }));
295
296 let output = explain(&schema, "enum-test", &plain());
297 assert!(output.contains("Values: low, medium, high"));
298 }
299
300 #[test]
301 fn required_properties_marked() {
302 let schema = sv(json!({
303 "type": "object",
304 "required": ["name"],
305 "properties": {
306 "name": {
307 "type": "string"
308 },
309 "optional": {
310 "type": "string"
311 }
312 }
313 }));
314
315 let output = explain(&schema, "required-test", &plain());
316 assert!(output.contains("name (string, *required)"));
317 assert!(output.contains("optional (string)"));
318 assert!(!output.contains("optional (string, *required)"));
319
320 let name_pos = output
322 .find("name (string")
323 .expect("name field should be present");
324 let optional_pos = output
325 .find("optional (string")
326 .expect("optional field should be present");
327 assert!(
328 name_pos < optional_pos,
329 "required field 'name' should appear before optional field"
330 );
331 }
332
333 #[test]
334 fn schema_with_no_properties_handled() {
335 let schema = sv(json!({
336 "type": "string",
337 "description": "A plain string type"
338 }));
339
340 let output = explain(&schema, "simple", &plain());
341 assert!(!output.contains("TITLE"));
342 assert!(output.contains("DESCRIPTION"));
343 assert!(output.contains("A plain string type"));
344 assert!(!output.contains("PROPERTIES"));
345 }
346
347 #[test]
348 fn color_output_contains_ansi() {
349 let schema = sv(json!({
350 "title": "Colored",
351 "type": "object",
352 "properties": {
353 "x": { "type": "string" }
354 }
355 }));
356
357 let colored_out = explain(&schema, "colored", &colored());
358 let plain_out = explain(&schema, "colored", &plain());
359
360 assert!(colored_out.contains(BOLD));
361 assert!(colored_out.contains(RESET));
362 assert!(colored_out.contains(CYAN));
363 assert!(colored_out.contains(GREEN));
364 assert!(!plain_out.contains(BOLD));
365 assert!(!plain_out.contains(RESET));
366 }
367
368 #[test]
369 fn default_value_shown() {
370 let schema = sv(json!({
371 "type": "object",
372 "properties": {
373 "port": {
374 "type": "integer",
375 "default": 8080
376 }
377 }
378 }));
379
380 let output = explain(&schema, "defaults", &plain());
381 assert!(output.contains("Default: 8080"));
382 }
383
384 #[test]
385 fn long_default_wraps() {
386 let long_val = "First of: `tsconfig.json` rootDir if specified, directory containing `tsconfig.json`, or cwd if no `tsconfig.json` is loaded.";
387 let schema = sv(json!({
388 "type": "object",
389 "properties": {
390 "declarationDir": {
391 "type": "string",
392 "default": long_val
393 }
394 }
395 }));
396
397 let output = explain(&schema, "wrap-test", &plain());
398 assert!(
401 output.contains("Default:\n"),
402 "long default should wrap onto next line\n{output}"
403 );
404 assert!(
405 output.contains(long_val),
406 "full default value should appear in output\n{output}"
407 );
408 }
409
410 #[test]
411 fn ref_resolution() {
412 let schema = sv(json!({
413 "type": "object",
414 "properties": {
415 "item": { "$ref": "#/$defs/Item" }
416 },
417 "$defs": {
418 "Item": {
419 "type": "object",
420 "description": "An item definition"
421 }
422 }
423 }));
424
425 let output = explain(&schema, "ref-test", &plain());
426 assert!(output.contains("item (object)"));
427 assert!(output.contains("An item definition"));
428 }
429
430 #[test]
431 fn format_header_centers() {
432 let h = format_header("TEST", "JSON Schema", 76);
433 assert!(h.starts_with("TEST"));
434 assert!(h.ends_with("TEST"));
435 assert!(h.contains("JSON Schema"));
436 assert_eq!(h.len(), 76);
437 }
438
439 #[test]
440 fn format_header_uses_full_width() {
441 let h = format_header("CARGO MANIFEST", "JSON Schema", 120);
442 assert_eq!(h.len(), 120);
443 assert!(h.starts_with("CARGO MANIFEST"));
444 assert!(h.ends_with("CARGO MANIFEST"));
445 }
446
447 #[test]
448 fn explain_output_uses_width() {
449 let schema = sv(json!({"type": "object", "title": "Test"}));
450 let opts_120 = ExplainOptions {
451 width: 120,
452 ..plain()
453 };
454 let output_80 = explain(&schema, "test", &plain());
455 let output_120 = explain(&schema, "test", &opts_120);
456 let header_80 = output_80.lines().next().unwrap();
457 let header_120 = output_120.lines().next().unwrap();
458 assert_eq!(header_80.len(), 80);
459 assert_eq!(header_120.len(), 120);
460 }
461
462 #[test]
463 fn inline_backtick_colorization() {
464 let f = Fmt::color(80);
465 let result = markdown_to_ansi::render_inline("Use `foo` and `bar`", &f.md_opts(None));
466 assert!(result.contains(BLUE));
467 assert!(result.contains("foo"));
468 assert!(result.contains("bar"));
469 assert!(!result.contains('`'));
470 }
471
472 #[test]
473 fn inline_bold_rendering() {
474 let f = Fmt::color(80);
475 let result =
476 markdown_to_ansi::render_inline("This is **important** text", &f.md_opts(None));
477 assert!(result.contains(BOLD));
478 assert!(result.contains("important"));
479 assert!(!result.contains("**"));
480 }
481
482 #[test]
483 fn inline_markdown_link() {
484 let f = Fmt::color(80);
485 let result = markdown_to_ansi::render_inline(
486 "See [docs](https://example.com) here",
487 &f.md_opts(None),
488 );
489 assert!(result.contains("docs"));
490 assert!(result.contains("https://example.com"));
491 assert!(result.contains("\x1b]8;;"));
492 }
493
494 #[test]
495 fn inline_raw_url() {
496 let f = Fmt::color(80);
497 let result =
498 markdown_to_ansi::render_inline("See more: https://example.com/foo", &f.md_opts(None));
499 assert!(result.contains("https://example.com/foo"));
500 }
501
502 #[test]
503 fn type_formatting_union() {
504 let f = Fmt::plain(80);
505 let result = format_type("object | null", &f);
506 assert!(result.contains("object"));
507 assert!(result.contains("null"));
508 assert!(result.contains('|'));
509 }
510
511 #[test]
512 fn prefers_markdown_description() {
513 let schema = sv(json!({
514 "type": "object",
515 "properties": {
516 "target": {
517 "type": "string",
518 "description": "Plain description",
519 "markdownDescription": "Rich **markdown** description"
520 }
521 }
522 }));
523
524 let output = explain(&schema, "test", &plain());
525 assert!(output.contains("Rich **markdown** description"));
526 assert!(!output.contains("Plain description"));
527 }
528
529 #[test]
530 fn no_premature_wrapping() {
531 let schema = sv(json!({
532 "type": "object",
533 "properties": {
534 "x": {
535 "type": "string",
536 "description": "This is a very long description that should not be wrapped at 72 characters because we want the pager to handle wrapping at the terminal width instead"
537 }
538 }
539 }));
540
541 let output = explain(&schema, "test", &plain());
542 let desc_line = output
543 .lines()
544 .find(|l| l.contains("This is a very long"))
545 .expect("description line should be present");
546 assert!(desc_line.contains("terminal width instead"));
547 }
548
549 #[test]
552 fn explain_at_path_shows_sub_schema() {
553 let schema = sv(json!({
554 "type": "object",
555 "properties": {
556 "name": {
557 "type": "string",
558 "description": "The name field"
559 },
560 "config": {
561 "type": "object",
562 "title": "Config",
563 "description": "Configuration settings",
564 "properties": {
565 "debug": { "type": "boolean" }
566 }
567 }
568 }
569 }));
570
571 let output = explain_at_path(&schema, "/properties/config", "test", &plain()).unwrap();
572 assert!(output.contains("Config"));
573 assert!(output.contains("Configuration settings"));
574 assert!(output.contains("debug (boolean)"));
575 assert!(!output.contains("The name field"));
577 }
578
579 #[test]
580 fn explain_at_path_root_pointer_shows_full_schema() {
581 let schema = sv(json!({
582 "type": "object",
583 "title": "Root",
584 "properties": {
585 "a": { "type": "string" }
586 }
587 }));
588
589 let output = explain_at_path(&schema, "", "test", &plain()).unwrap();
590 assert!(output.contains("Root"));
591 assert!(output.contains("a (string)"));
592 }
593
594 #[test]
595 fn explain_at_path_resolves_ref() {
596 let schema = sv(json!({
597 "type": "object",
598 "properties": {
599 "item": { "$ref": "#/$defs/Item" }
600 },
601 "$defs": {
602 "Item": {
603 "type": "object",
604 "title": "Item",
605 "description": "An item",
606 "properties": {
607 "id": { "type": "integer" }
608 }
609 }
610 }
611 }));
612
613 let output = explain_at_path(&schema, "/properties/item", "test", &plain()).unwrap();
614 assert!(output.contains("Item"));
615 assert!(output.contains("An item"));
616 assert!(output.contains("id (integer)"));
617 }
618
619 #[test]
620 fn explain_at_path_bad_pointer_errors() {
621 let schema = sv(json!({"type": "object"}));
622 let err = explain_at_path(&schema, "/nonexistent/path", "test", &plain());
623 assert!(err.is_err());
624 assert!(err.unwrap_err().contains("nonexistent"));
625 }
626
627 #[test]
628 fn property_examples_shown() {
629 let schema = sv(json!({
630 "type": "object",
631 "properties": {
632 "name": {
633 "type": "string",
634 "examples": ["TAG-ID", "DUNS"]
635 }
636 }
637 }));
638
639 let output = explain(&schema, "examples-test", &plain());
640 assert!(output.contains("Examples: \"TAG-ID\", \"DUNS\""));
641 }
642
643 #[test]
644 fn explain_at_path_deep_nesting() {
645 let schema = sv(json!({
646 "type": "object",
647 "properties": {
648 "a": {
649 "type": "object",
650 "properties": {
651 "b": {
652 "type": "object",
653 "title": "Deep",
654 "properties": {
655 "c": { "type": "string", "description": "Deeply nested" }
656 }
657 }
658 }
659 }
660 }
661 }));
662
663 let output =
664 explain_at_path(&schema, "/properties/a/properties/b", "test", &plain()).unwrap();
665 assert!(output.contains("Deep"));
666 assert!(output.contains("c (string)"));
667 assert!(output.contains("Deeply nested"));
668 }
669
670 #[test]
671 fn numeric_constraints_shown() {
672 let schema = sv(json!({
673 "type": "object",
674 "properties": {
675 "port": {
676 "type": "integer",
677 "minimum": 1,
678 "maximum": 65535
679 }
680 }
681 }));
682
683 let output = explain(&schema, "constraints", &plain());
684 assert!(output.contains("Constraints:"));
685 assert!(output.contains("min=1"));
686 assert!(output.contains("max=65535"));
687 }
688
689 #[test]
690 fn string_constraints_shown() {
691 let schema = sv(json!({
692 "type": "object",
693 "properties": {
694 "email": {
695 "type": "string",
696 "format": "email",
697 "minLength": 5,
698 "maxLength": 255,
699 "pattern": "^[^@]+@[^@]+$"
700 }
701 }
702 }));
703
704 let output = explain(&schema, "constraints", &plain());
705 assert!(output.contains("format=email"));
706 assert!(output.contains("minLength=5"));
707 assert!(output.contains("maxLength=255"));
708 assert!(output.contains("pattern=^[^@]+@[^@]+$"));
709 }
710
711 #[test]
712 fn array_constraints_shown() {
713 let schema = sv(json!({
714 "type": "object",
715 "properties": {
716 "tags": {
717 "type": "array",
718 "items": { "type": "string" },
719 "minItems": 1,
720 "maxItems": 10,
721 "uniqueItems": true
722 }
723 }
724 }));
725
726 let output = explain(&schema, "constraints", &plain());
727 assert!(output.contains("minItems=1"));
728 assert!(output.contains("maxItems=10"));
729 assert!(output.contains("unique"));
730 }
731
732 #[test]
733 fn exclusive_bounds_and_multiple_of_shown() {
734 let schema = sv(json!({
735 "type": "object",
736 "properties": {
737 "score": {
738 "type": "number",
739 "exclusiveMinimum": 0,
740 "exclusiveMaximum": 100,
741 "multipleOf": 0.5
742 }
743 }
744 }));
745
746 let output = explain(&schema, "constraints", &plain());
747 assert!(output.contains("exclusiveMin=0"));
748 assert!(output.contains("exclusiveMax=100"));
749 assert!(output.contains("multipleOf=0.5"));
750 }
751
752 #[test]
753 fn no_constraints_line_when_none() {
754 let schema = sv(json!({
755 "type": "object",
756 "properties": {
757 "name": {
758 "type": "string",
759 "description": "Just a name"
760 }
761 }
762 }));
763
764 let output = explain(&schema, "no-constraints", &plain());
765 assert!(!output.contains("Constraints:"));
766 }
767
768 #[test]
771 fn title_section_shows_schema_title() {
772 let schema = sv(json!({
773 "title": "My Schema",
774 "type": "object"
775 }));
776
777 let output = explain(&schema, "display-name", &plain());
778 assert!(output.contains("TITLE"));
779 assert!(output.contains("My Schema"));
780 }
781
782 #[test]
783 fn title_section_hidden_without_schema_title() {
784 let schema = sv(json!({ "type": "object" }));
785
786 let output = explain(&schema, "fallback-name", &plain());
787 assert!(!output.contains("TITLE"));
788 assert!(output.contains("fallback-name"));
790 }
791
792 #[test]
793 fn schema_section_appears_after_description() {
794 let schema = sv(json!({
795 "$id": "https://json.schemastore.org/cargo.json",
796 "type": "object",
797 "title": "Cargo",
798 "description": "Cargo manifest schema"
799 }));
800
801 let output = explain(&schema, "cargo", &plain());
802 let desc_pos = output.find("DESCRIPTION").unwrap();
803 let schema_pos = output.find("SCHEMA").unwrap();
804 let type_pos = output.find("TYPE").unwrap();
805 assert!(
806 desc_pos < schema_pos,
807 "SCHEMA should appear after DESCRIPTION"
808 );
809 assert!(schema_pos < type_pos, "SCHEMA should appear before TYPE");
810 }
811
812 #[test]
815 fn comment_shown() {
816 let schema = sv(json!({
817 "type": "object",
818 "properties": {
819 "x": {
820 "type": "string",
821 "$comment": "See https://example.com for details"
822 }
823 }
824 }));
825
826 let extended = ExplainOptions {
827 extended: true,
828 ..plain()
829 };
830 let output = explain(&schema, "comment-test", &extended);
831 assert!(output.contains("Comment:"));
832 assert!(output.contains("See https://example.com for details"));
833 }
834
835 #[test]
836 fn comment_hidden_by_default() {
837 let schema = sv(json!({
838 "type": "object",
839 "$comment": "Hidden comment",
840 "properties": {
841 "x": {
842 "type": "string",
843 "$comment": "Also hidden"
844 }
845 }
846 }));
847
848 let output = explain(&schema, "comment-test", &plain());
849 assert!(!output.contains("Comment"));
850 assert!(!output.contains("Hidden comment"));
851 assert!(!output.contains("Also hidden"));
852 }
853
854 #[test]
855 fn root_comment_shown() {
856 let schema = sv(json!({
857 "$comment": "Root level comment",
858 "type": "object"
859 }));
860
861 let extended = ExplainOptions {
862 extended: true,
863 ..plain()
864 };
865 let output = explain(&schema, "comment-test", &extended);
866 assert!(output.contains("COMMENT"));
867 assert!(output.contains("Root level comment"));
868 assert!(
870 !output.contains("\n\n\n"),
871 "should not have triple newlines (double blank lines)\n{output}"
872 );
873 }
874
875 #[test]
876 fn additional_properties_false() {
877 let schema = sv(json!({
878 "type": "object",
879 "properties": {
880 "name": { "type": "string" }
881 },
882 "additionalProperties": false
883 }));
884
885 let output = explain(&schema, "ap-test", &plain());
886 assert!(output.contains("Additional properties: not allowed"));
887 }
888
889 #[test]
890 fn additional_properties_schema() {
891 let schema = sv(json!({
892 "type": "object",
893 "properties": {
894 "name": { "type": "string" }
895 },
896 "additionalProperties": { "type": "string" }
897 }));
898
899 let output = explain(&schema, "ap-test", &plain());
900 assert!(output.contains("Additional properties: string"));
901 }
902
903 #[test]
904 fn additional_properties_true_not_shown() {
905 let schema = sv(json!({
906 "type": "object",
907 "additionalProperties": true
908 }));
909
910 let output = explain(&schema, "ap-test", &plain());
911 assert!(!output.contains("Additional properties"));
912 }
913
914 #[test]
915 fn pattern_properties_shown() {
916 let schema = sv(json!({
917 "type": "object",
918 "patternProperties": {
919 "^x-": { "type": "object", "description": "Extension properties" }
920 }
921 }));
922
923 let output = explain(&schema, "pp-test", &plain());
924 assert!(output.contains("Pattern properties:"));
925 assert!(output.contains("^x-"));
926 assert!(output.contains("Extension properties"));
927 }
928
929 #[test]
930 fn if_then_else_shown() {
931 let schema = sv(json!({
932 "type": "object",
933 "if": { "properties": { "type": { "const": "a" } } },
934 "then": { "properties": { "value": { "type": "string" } } },
935 "else": { "properties": { "value": { "type": "integer" } } }
936 }));
937
938 let output = explain(&schema, "cond-test", &plain());
939 assert!(output.contains("CONDITIONAL"));
940 assert!(output.contains("If:"));
941 assert!(output.contains("Then:"));
942 assert!(output.contains("Else:"));
943 }
944
945 #[test]
946 fn not_shown() {
947 let schema = sv(json!({
948 "type": "object",
949 "properties": {
950 "x": {
951 "not": { "type": "string" }
952 }
953 }
954 }));
955
956 let output = explain(&schema, "not-test", &plain());
957 assert!(output.contains("Not: string"));
958 }
959
960 #[test]
961 fn dependent_required_shown() {
962 let schema = sv(json!({
963 "type": "object",
964 "properties": {
965 "config": {
966 "type": "object",
967 "dependentRequired": {
968 "bar": ["foo"]
969 }
970 }
971 }
972 }));
973
974 let output = explain(&schema, "dr-test", &plain());
975 assert!(output.contains("Dependent required:"));
976 assert!(output.contains("\"bar\""));
977 assert!(output.contains("\"foo\""));
978 }
979
980 #[test]
981 fn property_names_shown() {
982 let schema = sv(json!({
983 "type": "object",
984 "properties": {
985 "config": {
986 "type": "object",
987 "propertyNames": { "pattern": "^[a-z]+$" }
988 }
989 }
990 }));
991
992 let output = explain(&schema, "pn-test", &plain());
993 assert!(output.contains("Property names: pattern=^[a-z]+$"));
994 }
995
996 #[test]
997 fn prefix_items_shown() {
998 let schema = sv(json!({
999 "type": "object",
1000 "properties": {
1001 "tuple": {
1002 "type": "array",
1003 "prefixItems": [
1004 { "type": "string" },
1005 { "type": "integer" }
1006 ]
1007 }
1008 }
1009 }));
1010
1011 let output = explain(&schema, "prefix-test", &plain());
1012 assert!(output.contains("Tuple items:"));
1013 assert!(output.contains("[0]: string"));
1014 assert!(output.contains("[1]: integer"));
1015 }
1016
1017 #[test]
1018 fn contains_shown() {
1019 let schema = sv(json!({
1020 "type": "object",
1021 "properties": {
1022 "arr": {
1023 "type": "array",
1024 "contains": { "type": "string" },
1025 "minContains": 1
1026 }
1027 }
1028 }));
1029
1030 let output = explain(&schema, "contains-test", &plain());
1031 assert!(output.contains("Contains: string"));
1032 assert!(output.contains("minContains=1"));
1033 }
1034
1035 #[test]
1036 fn read_only_tag_shown() {
1037 let schema = sv(json!({
1038 "type": "object",
1039 "properties": {
1040 "id": {
1041 "type": "string",
1042 "readOnly": true
1043 }
1044 }
1045 }));
1046
1047 let output = explain(&schema, "ro-test", &plain());
1048 assert!(output.contains("[READ-ONLY]"));
1049 }
1050
1051 #[test]
1052 fn write_only_tag_shown() {
1053 let schema = sv(json!({
1054 "type": "object",
1055 "properties": {
1056 "password": {
1057 "type": "string",
1058 "writeOnly": true
1059 }
1060 }
1061 }));
1062
1063 let output = explain(&schema, "wo-test", &plain());
1064 assert!(output.contains("[WRITE-ONLY]"));
1065 }
1066
1067 #[test]
1068 fn content_media_type_shown() {
1069 let schema = sv(json!({
1070 "type": "object",
1071 "properties": {
1072 "data": {
1073 "type": "string",
1074 "contentMediaType": "application/json",
1075 "contentEncoding": "base64"
1076 }
1077 }
1078 }));
1079
1080 let output = explain(&schema, "content-test", &plain());
1081 assert!(output.contains("Content: application/json (base64)"));
1082 }
1083
1084 #[test]
1085 fn markdown_enum_descriptions_shown() {
1086 let schema = sv(json!({
1087 "type": "object",
1088 "properties": {
1089 "mode": {
1090 "type": "string",
1091 "enum": ["fast", "safe", "auto"],
1092 "markdownEnumDescriptions": [
1093 "Optimizes for speed",
1094 "Optimizes for safety",
1095 "Automatically chooses"
1096 ]
1097 }
1098 }
1099 }));
1100
1101 let output = explain(&schema, "enum-desc-test", &plain());
1102 assert!(output.contains("Values:"));
1103 assert!(output.contains("fast"));
1104 assert!(output.contains("Optimizes for speed"));
1105 assert!(output.contains("—"));
1106 }
1107
1108 #[test]
1109 fn min_max_contains_in_constraints() {
1110 let schema = sv(json!({
1111 "type": "object",
1112 "properties": {
1113 "arr": {
1114 "type": "array",
1115 "minContains": 2,
1116 "maxContains": 5
1117 }
1118 }
1119 }));
1120
1121 let output = explain(&schema, "contains-constraints", &plain());
1122 assert!(output.contains("minContains=2"));
1123 assert!(output.contains("maxContains=5"));
1124 }
1125
1126 #[test]
1127 fn dependent_schemas_shown() {
1128 let schema = sv(json!({
1129 "type": "object",
1130 "properties": {
1131 "config": {
1132 "type": "object",
1133 "dependentSchemas": {
1134 "credit_card": {
1135 "properties": {
1136 "billing_address": { "type": "string" }
1137 }
1138 }
1139 }
1140 }
1141 }
1142 }));
1143
1144 let output = explain(&schema, "ds-test", &plain());
1145 assert!(output.contains("Dependent schemas:"));
1146 assert!(output.contains("\"credit_card\""));
1147 }
1148}