1use std::path::Path;
8use std::sync::Arc;
9
10use crate::error::{Error, Result};
11use crate::value::Value;
12
13#[derive(Debug, Clone)]
15pub struct Schema {
16 schema: serde_json::Value,
18 compiled: Arc<jsonschema::Validator>,
20}
21
22impl Schema {
23 pub fn from_json(json: &str) -> Result<Self> {
25 let schema: serde_json::Value = serde_json::from_str(json)
26 .map_err(|e| Error::parse(format!("Invalid JSON schema: {}", e)))?;
27 Self::from_value(schema)
28 }
29
30 pub fn from_yaml(yaml: &str) -> Result<Self> {
32 let schema: serde_json::Value = serde_yaml::from_str(yaml)
33 .map_err(|e| Error::parse(format!("Invalid YAML schema: {}", e)))?;
34 Self::from_value(schema)
35 }
36
37 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
39 let path = path.as_ref();
40 let content = std::fs::read_to_string(path)
41 .map_err(|_| Error::file_not_found(path.display().to_string(), None))?;
42
43 match path.extension().and_then(|e| e.to_str()) {
44 Some("json") => Self::from_json(&content),
45 Some("yaml") | Some("yml") => Self::from_yaml(&content),
46 _ => Self::from_yaml(&content), }
48 }
49
50 fn from_value(schema: serde_json::Value) -> Result<Self> {
52 let compiled = jsonschema::validator_for(&schema)
53 .map_err(|e| Error::parse(format!("Invalid JSON Schema: {}", e)))?;
54 Ok(Self {
55 schema,
56 compiled: Arc::new(compiled),
57 })
58 }
59
60 pub fn validate(&self, value: &Value) -> Result<()> {
64 let json_value = value_to_json(value);
66
67 let mut errors = self.compiled.iter_errors(&json_value);
69 if let Some(error) = errors.next() {
70 let path = error.instance_path.to_string();
71 let message = error.to_string();
72 return Err(Error::validation(
73 if path.is_empty() { "<root>" } else { &path },
74 &message,
75 ));
76 }
77 Ok(())
78 }
79
80 pub fn validate_collect(&self, value: &Value) -> Vec<ValidationError> {
82 let json_value = value_to_json(value);
83
84 self.compiled
85 .iter_errors(&json_value)
86 .map(|e| ValidationError {
87 path: e.instance_path.to_string(),
88 message: e.to_string(),
89 })
90 .collect()
91 }
92
93 pub fn as_value(&self) -> &serde_json::Value {
95 &self.schema
96 }
97
98 pub fn to_yaml(&self) -> Result<String> {
100 serde_yaml::to_string(&self.schema)
101 .map_err(|e| Error::internal(format!("Failed to serialize schema: {}", e)))
102 }
103
104 pub fn to_json(&self) -> Result<String> {
106 serde_json::to_string_pretty(&self.schema)
107 .map_err(|e| Error::internal(format!("Failed to serialize schema: {}", e)))
108 }
109
110 pub fn to_markdown(&self) -> String {
112 generate_markdown_doc(&self.schema)
113 }
114
115 pub fn to_template(&self) -> String {
120 generate_template(&self.schema)
121 }
122
123 pub fn get_default(&self, path: &str) -> Option<Value> {
146 if path.is_empty() {
147 return self.schema.get("default").map(json_to_value);
148 }
149
150 let segments: Vec<&str> = path.split('.').collect();
151 self.get_default_at_path(&self.schema, &segments)
152 }
153
154 fn get_default_at_path(&self, schema: &serde_json::Value, segments: &[&str]) -> Option<Value> {
156 if segments.is_empty() {
157 return schema.get("default").map(json_to_value);
159 }
160
161 let segment = segments[0];
162 let remaining = &segments[1..];
163
164 if let Some(properties) = schema.get("properties") {
166 if let Some(prop_schema) = properties.get(segment) {
167 return self.get_default_at_path(prop_schema, remaining);
168 }
169 }
170
171 None
172 }
173
174 pub fn allows_null(&self, path: &str) -> bool {
197 if path.is_empty() {
198 return self.type_allows_null(&self.schema);
199 }
200
201 let segments: Vec<&str> = path.split('.').collect();
202 self.allows_null_at_path(&self.schema, &segments)
203 }
204
205 fn allows_null_at_path(&self, schema: &serde_json::Value, segments: &[&str]) -> bool {
207 if segments.is_empty() {
208 return self.type_allows_null(schema);
209 }
210
211 let segment = segments[0];
212 let remaining = &segments[1..];
213
214 if let Some(properties) = schema.get("properties") {
216 if let Some(prop_schema) = properties.get(segment) {
217 return self.allows_null_at_path(prop_schema, remaining);
218 }
219 }
220
221 true
223 }
224
225 fn type_allows_null(&self, schema: &serde_json::Value) -> bool {
227 match schema.get("type") {
228 Some(serde_json::Value::String(t)) => t == "null",
229 Some(serde_json::Value::Array(types)) => {
230 types.iter().any(|t| t.as_str() == Some("null"))
231 }
232 None => true, _ => false, }
235 }
236}
237
238#[derive(Debug, Clone)]
240pub struct ValidationError {
241 pub path: String,
243 pub message: String,
245}
246
247impl std::fmt::Display for ValidationError {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 if self.path.is_empty() {
250 write!(f, "{}", self.message)
251 } else {
252 write!(f, "{}: {}", self.path, self.message)
253 }
254 }
255}
256
257fn generate_markdown_doc(schema: &serde_json::Value) -> String {
259 let mut output = String::new();
260
261 let title = schema
263 .get("title")
264 .and_then(|v| v.as_str())
265 .unwrap_or("Configuration Reference");
266 output.push_str(&format!("# {}\n\n", title));
267
268 if let Some(desc) = schema.get("description").and_then(|v| v.as_str()) {
270 output.push_str(&format!("{}\n\n", desc));
271 }
272
273 let root_required: Vec<&str> = schema
275 .get("required")
276 .and_then(|v| v.as_array())
277 .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
278 .unwrap_or_default();
279
280 if let Some(properties) = schema.get("properties").and_then(|v| v.as_object()) {
282 for (name, prop_schema) in properties {
283 generate_section(
284 &mut output,
285 name,
286 prop_schema,
287 root_required.contains(&name.as_str()),
288 2,
289 );
290 }
291 }
292
293 output
294}
295
296fn generate_section(
298 output: &mut String,
299 name: &str,
300 schema: &serde_json::Value,
301 is_required: bool,
302 heading_level: usize,
303) {
304 let heading = "#".repeat(heading_level);
305 let required_marker = if is_required { " (required)" } else { "" };
306
307 output.push_str(&format!("{} {}{}\n\n", heading, name, required_marker));
309
310 if let Some(desc) = schema.get("description").and_then(|v| v.as_str()) {
312 output.push_str(&format!("{}\n\n", desc));
313 }
314
315 let is_object = schema
317 .get("type")
318 .and_then(|v| v.as_str())
319 .map(|t| t == "object")
320 .unwrap_or(false);
321
322 if is_object {
323 if let Some(properties) = schema.get("properties").and_then(|v| v.as_object()) {
324 let required: Vec<&str> = schema
326 .get("required")
327 .and_then(|v| v.as_array())
328 .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
329 .unwrap_or_default();
330
331 output.push_str("| Key | Type | Required | Default | Description |\n");
333 output.push_str("|-----|------|----------|---------|-------------|\n");
334
335 for (prop_name, prop_schema) in properties {
336 let prop_type = get_type_string(prop_schema);
337 let prop_required = if required.contains(&prop_name.as_str()) {
338 "Yes"
339 } else {
340 "No"
341 };
342 let prop_default = schema_default_string(prop_schema);
343 let prop_desc = prop_schema
344 .get("description")
345 .and_then(|v| v.as_str())
346 .unwrap_or("-");
347
348 output.push_str(&format!(
349 "| {} | {} | {} | {} | {} |\n",
350 prop_name, prop_type, prop_required, prop_default, prop_desc
351 ));
352 }
353
354 output.push('\n');
355
356 for (prop_name, prop_schema) in properties {
358 let prop_is_object = prop_schema
359 .get("type")
360 .and_then(|v| v.as_str())
361 .map(|t| t == "object")
362 .unwrap_or(false);
363
364 if prop_is_object && prop_schema.get("properties").is_some() {
365 let nested_required = required.contains(&prop_name.as_str());
366 generate_section(
367 output,
368 prop_name,
369 prop_schema,
370 nested_required,
371 heading_level + 1,
372 );
373 }
374 }
375 }
376 } else {
377 output.push_str("| Key | Type | Required | Default | Description |\n");
379 output.push_str("|-----|------|----------|---------|-------------|\n");
380
381 let prop_type = get_type_string(schema);
382 let prop_required = if is_required { "Yes" } else { "No" };
383 let prop_default = schema_default_string(schema);
384 let prop_desc = schema
385 .get("description")
386 .and_then(|v| v.as_str())
387 .unwrap_or("-");
388
389 output.push_str(&format!(
390 "| {} | {} | {} | {} | {} |\n\n",
391 name, prop_type, prop_required, prop_default, prop_desc
392 ));
393 }
394}
395
396fn get_type_string(schema: &serde_json::Value) -> String {
398 if let Some(enum_vals) = schema.get("enum").and_then(|v| v.as_array()) {
400 let vals: Vec<String> = enum_vals
401 .iter()
402 .map(|v| {
403 if v.is_string() {
404 format!("\"{}\"", v.as_str().unwrap())
405 } else {
406 v.to_string()
407 }
408 })
409 .collect();
410 return format!("enum: {}", vals.join(", "));
411 }
412
413 let base_type = schema.get("type").and_then(|v| v.as_str()).unwrap_or("any");
415
416 let mut constraints = Vec::new();
418
419 if let Some(min) = schema.get("minimum") {
420 constraints.push(format!("min: {}", min));
421 }
422 if let Some(max) = schema.get("maximum") {
423 constraints.push(format!("max: {}", max));
424 }
425 if let Some(pattern) = schema.get("pattern").and_then(|v| v.as_str()) {
426 constraints.push(format!("pattern: {}", pattern));
427 }
428 if let Some(min_len) = schema.get("minLength") {
429 constraints.push(format!("minLength: {}", min_len));
430 }
431 if let Some(max_len) = schema.get("maxLength") {
432 constraints.push(format!("maxLength: {}", max_len));
433 }
434
435 if constraints.is_empty() {
436 base_type.to_string()
437 } else {
438 format!("{} ({})", base_type, constraints.join(", "))
439 }
440}
441
442fn schema_default_string(schema: &serde_json::Value) -> String {
444 schema
445 .get("default")
446 .map(|v| {
447 if v.is_string() {
448 format!("\"{}\"", v.as_str().unwrap())
449 } else {
450 v.to_string()
451 }
452 })
453 .unwrap_or_else(|| "-".to_string())
454}
455
456fn json_to_value(json: &serde_json::Value) -> Value {
458 match json {
459 serde_json::Value::Null => Value::Null,
460 serde_json::Value::Bool(b) => Value::Bool(*b),
461 serde_json::Value::Number(n) => {
462 if let Some(i) = n.as_i64() {
463 Value::Integer(i)
464 } else if let Some(f) = n.as_f64() {
465 Value::Float(f)
466 } else {
467 Value::Float(n.as_f64().unwrap_or(0.0))
469 }
470 }
471 serde_json::Value::String(s) => Value::String(s.clone()),
472 serde_json::Value::Array(arr) => Value::Sequence(arr.iter().map(json_to_value).collect()),
473 serde_json::Value::Object(obj) => {
474 let map: indexmap::IndexMap<String, Value> = obj
475 .iter()
476 .map(|(k, v)| (k.clone(), json_to_value(v)))
477 .collect();
478 Value::Mapping(map)
479 }
480 }
481}
482
483fn value_to_json(value: &Value) -> serde_json::Value {
485 match value {
486 Value::Null => serde_json::Value::Null,
487 Value::Bool(b) => serde_json::Value::Bool(*b),
488 Value::Integer(i) => serde_json::Value::Number((*i).into()),
489 Value::Float(f) => serde_json::Number::from_f64(*f)
490 .map(serde_json::Value::Number)
491 .unwrap_or(serde_json::Value::Null),
492 Value::String(s) => serde_json::Value::String(s.clone()),
493 Value::Bytes(bytes) => {
494 use base64::{engine::general_purpose::STANDARD, Engine as _};
496 serde_json::Value::String(STANDARD.encode(bytes))
497 }
498 Value::Sequence(seq) => serde_json::Value::Array(seq.iter().map(value_to_json).collect()),
499 Value::Mapping(map) => {
500 let obj: serde_json::Map<String, serde_json::Value> = map
501 .iter()
502 .map(|(k, v)| (k.clone(), value_to_json(v)))
503 .collect();
504 serde_json::Value::Object(obj)
505 }
506 }
507}
508
509fn generate_template(schema: &serde_json::Value) -> String {
511 let mut output = String::new();
512
513 if let Some(title) = schema.get("title").and_then(|v| v.as_str()) {
515 output.push_str(&format!("# Generated from: {}\n", title));
516 } else {
517 output.push_str("# Configuration template generated from schema\n");
518 }
519 output.push_str("# Required fields marked with # REQUIRED\n\n");
520
521 let root_required: Vec<&str> = schema
523 .get("required")
524 .and_then(|v| v.as_array())
525 .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
526 .unwrap_or_default();
527
528 if let Some(properties) = schema.get("properties").and_then(|v| v.as_object()) {
530 for (name, prop_schema) in properties {
531 let is_required = root_required.contains(&name.as_str());
532 generate_template_property(&mut output, name, prop_schema, is_required, 0);
533 }
534 }
535
536 output
537}
538
539fn generate_template_property(
541 output: &mut String,
542 name: &str,
543 schema: &serde_json::Value,
544 is_required: bool,
545 indent_level: usize,
546) {
547 let indent = " ".repeat(indent_level);
548
549 let mut comment_parts = Vec::new();
551 if is_required {
552 comment_parts.push("REQUIRED".to_string());
553 }
554 if let Some(desc) = schema.get("description").and_then(|v| v.as_str()) {
555 comment_parts.push(desc.to_string());
556 }
557
558 let prop_type = schema.get("type").and_then(|v| v.as_str()).unwrap_or("any");
560
561 if prop_type == "object" {
563 if !comment_parts.is_empty() {
565 output.push_str(&format!("{}# {}\n", indent, comment_parts.join(" - ")));
566 }
567 output.push_str(&format!("{}{}:\n", indent, name));
568
569 let required: Vec<&str> = schema
571 .get("required")
572 .and_then(|v| v.as_array())
573 .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
574 .unwrap_or_default();
575
576 if let Some(properties) = schema.get("properties").and_then(|v| v.as_object()) {
578 for (prop_name, prop_schema) in properties {
579 let prop_required = required.contains(&prop_name.as_str());
580 generate_template_property(
581 output,
582 prop_name,
583 prop_schema,
584 prop_required,
585 indent_level + 1,
586 );
587 }
588 }
589 } else {
590 let value_str = get_template_value(schema, prop_type);
592
593 let mut line = format!("{}{}: {}", indent, name, value_str);
595
596 if let Some(default) = schema.get("default") {
598 if !comment_parts.is_empty() {
599 line.push_str(&format!(
600 " # {} (default: {})",
601 comment_parts.join(" - "),
602 format_json_value(default)
603 ));
604 } else {
605 line.push_str(&format!(" # default: {}", format_json_value(default)));
606 }
607 } else if !comment_parts.is_empty() {
608 line.push_str(&format!(" # {}", comment_parts.join(" - ")));
609 }
610
611 output.push_str(&line);
612 output.push('\n');
613 }
614}
615
616fn get_template_value(schema: &serde_json::Value, prop_type: &str) -> String {
618 if let Some(default) = schema.get("default") {
620 return format_json_value(default);
621 }
622
623 if let Some(enum_vals) = schema.get("enum").and_then(|v| v.as_array()) {
625 if let Some(first) = enum_vals.first() {
626 return format_json_value(first);
627 }
628 }
629
630 match prop_type {
632 "string" => "\"\"".to_string(),
633 "integer" => "0".to_string(),
634 "number" => "0.0".to_string(),
635 "boolean" => "false".to_string(),
636 "array" => "[]".to_string(),
637 "null" => "null".to_string(),
638 _ => "null".to_string(),
639 }
640}
641
642fn format_json_value(value: &serde_json::Value) -> String {
644 match value {
645 serde_json::Value::Null => "null".to_string(),
646 serde_json::Value::Bool(b) => b.to_string(),
647 serde_json::Value::Number(n) => n.to_string(),
648 serde_json::Value::String(s) => {
649 if s.is_empty()
651 || s.contains(':')
652 || s.contains('#')
653 || s.starts_with(' ')
654 || s.ends_with(' ')
655 {
656 format!("\"{}\"", s.replace('"', "\\\""))
657 } else {
658 s.clone()
659 }
660 }
661 serde_json::Value::Array(arr) => {
662 if arr.is_empty() {
663 "[]".to_string()
664 } else {
665 let items: Vec<String> = arr.iter().map(format_json_value).collect();
667 format!("[{}]", items.join(", "))
668 }
669 }
670 serde_json::Value::Object(_) => "{}".to_string(),
671 }
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677
678 #[test]
679 fn test_schema_from_yaml() {
680 let schema_yaml = r#"
681type: object
682required:
683 - name
684properties:
685 name:
686 type: string
687 port:
688 type: integer
689 minimum: 1
690 maximum: 65535
691"#;
692 let schema = Schema::from_yaml(schema_yaml).unwrap();
693 assert!(schema.as_value().is_object());
694 }
695
696 #[test]
697 fn test_validate_valid_config() {
698 let schema = Schema::from_yaml(
699 r#"
700type: object
701properties:
702 name:
703 type: string
704 port:
705 type: integer
706"#,
707 )
708 .unwrap();
709
710 let mut map = indexmap::IndexMap::new();
711 map.insert("name".into(), Value::String("myapp".into()));
712 map.insert("port".into(), Value::Integer(8080));
713 let config = Value::Mapping(map);
714
715 assert!(schema.validate(&config).is_ok());
716 }
717
718 #[test]
719 fn test_validate_missing_required() {
720 let schema = Schema::from_yaml(
721 r#"
722type: object
723required:
724 - name
725properties:
726 name:
727 type: string
728"#,
729 )
730 .unwrap();
731
732 let config = Value::Mapping(indexmap::IndexMap::new());
733 let result = schema.validate(&config);
734 assert!(result.is_err());
735 let err = result.unwrap_err();
736 assert!(err.to_string().contains("name"));
737 }
738
739 #[test]
740 fn test_validate_wrong_type() {
741 let schema = Schema::from_yaml(
742 r#"
743type: object
744properties:
745 port:
746 type: integer
747"#,
748 )
749 .unwrap();
750
751 let mut map = indexmap::IndexMap::new();
752 map.insert("port".into(), Value::String("not-a-number".into()));
753 let config = Value::Mapping(map);
754
755 let result = schema.validate(&config);
756 assert!(result.is_err());
757 }
758
759 #[test]
760 fn test_validate_constraint_violation() {
761 let schema = Schema::from_yaml(
762 r#"
763type: object
764properties:
765 port:
766 type: integer
767 minimum: 1
768 maximum: 65535
769"#,
770 )
771 .unwrap();
772
773 let mut map = indexmap::IndexMap::new();
774 map.insert("port".into(), Value::Integer(70000));
775 let config = Value::Mapping(map);
776
777 let result = schema.validate(&config);
778 assert!(result.is_err());
779 }
780
781 #[test]
782 fn test_validate_enum() {
783 let schema = Schema::from_yaml(
784 r#"
785type: object
786properties:
787 log_level:
788 type: string
789 enum: [debug, info, warn, error]
790"#,
791 )
792 .unwrap();
793
794 let mut map = indexmap::IndexMap::new();
796 map.insert("log_level".into(), Value::String("info".into()));
797 let config = Value::Mapping(map);
798 assert!(schema.validate(&config).is_ok());
799
800 let mut map = indexmap::IndexMap::new();
802 map.insert("log_level".into(), Value::String("verbose".into()));
803 let config = Value::Mapping(map);
804 assert!(schema.validate(&config).is_err());
805 }
806
807 #[test]
808 fn test_validate_nested() {
809 let schema = Schema::from_yaml(
810 r#"
811type: object
812properties:
813 database:
814 type: object
815 required: [host]
816 properties:
817 host:
818 type: string
819 port:
820 type: integer
821 default: 5432
822"#,
823 )
824 .unwrap();
825
826 let mut db = indexmap::IndexMap::new();
828 db.insert("host".into(), Value::String("localhost".into()));
829 db.insert("port".into(), Value::Integer(5432));
830 let mut map = indexmap::IndexMap::new();
831 map.insert("database".into(), Value::Mapping(db));
832 let config = Value::Mapping(map);
833 assert!(schema.validate(&config).is_ok());
834
835 let db = indexmap::IndexMap::new();
837 let mut map = indexmap::IndexMap::new();
838 map.insert("database".into(), Value::Mapping(db));
839 let config = Value::Mapping(map);
840 assert!(schema.validate(&config).is_err());
841 }
842
843 #[test]
844 fn test_validate_collect_multiple_errors() {
845 let schema = Schema::from_yaml(
846 r#"
847type: object
848required:
849 - name
850 - port
851properties:
852 name:
853 type: string
854 port:
855 type: integer
856"#,
857 )
858 .unwrap();
859
860 let config = Value::Mapping(indexmap::IndexMap::new());
861 let errors = schema.validate_collect(&config);
862 assert!(!errors.is_empty());
864 }
865
866 #[test]
867 fn test_validate_additional_properties_allowed() {
868 let schema = Schema::from_yaml(
870 r#"
871type: object
872properties:
873 name:
874 type: string
875"#,
876 )
877 .unwrap();
878
879 let mut map = indexmap::IndexMap::new();
880 map.insert("name".into(), Value::String("myapp".into()));
881 map.insert("extra".into(), Value::String("allowed".into()));
882 let config = Value::Mapping(map);
883 assert!(schema.validate(&config).is_ok());
884 }
885
886 #[test]
887 fn test_validate_additional_properties_denied() {
888 let schema = Schema::from_yaml(
889 r#"
890type: object
891properties:
892 name:
893 type: string
894additionalProperties: false
895"#,
896 )
897 .unwrap();
898
899 let mut map = indexmap::IndexMap::new();
900 map.insert("name".into(), Value::String("myapp".into()));
901 map.insert("extra".into(), Value::String("not allowed".into()));
902 let config = Value::Mapping(map);
903 assert!(schema.validate(&config).is_err());
904 }
905
906 #[test]
907 fn test_validate_array() {
908 let schema = Schema::from_yaml(
909 r#"
910type: object
911properties:
912 servers:
913 type: array
914 items:
915 type: string
916"#,
917 )
918 .unwrap();
919
920 let mut map = indexmap::IndexMap::new();
921 map.insert(
922 "servers".into(),
923 Value::Sequence(vec![
924 Value::String("server1".into()),
925 Value::String("server2".into()),
926 ]),
927 );
928 let config = Value::Mapping(map);
929 assert!(schema.validate(&config).is_ok());
930
931 let mut map = indexmap::IndexMap::new();
933 map.insert(
934 "servers".into(),
935 Value::Sequence(vec![Value::String("server1".into()), Value::Integer(123)]),
936 );
937 let config = Value::Mapping(map);
938 assert!(schema.validate(&config).is_err());
939 }
940
941 #[test]
942 fn test_validate_pattern() {
943 let schema = Schema::from_yaml(
944 r#"
945type: object
946properties:
947 version:
948 type: string
949 pattern: "^\\d+\\.\\d+\\.\\d+$"
950"#,
951 )
952 .unwrap();
953
954 let mut map = indexmap::IndexMap::new();
956 map.insert("version".into(), Value::String("1.2.3".into()));
957 let config = Value::Mapping(map);
958 assert!(schema.validate(&config).is_ok());
959
960 let mut map = indexmap::IndexMap::new();
962 map.insert("version".into(), Value::String("v1.2".into()));
963 let config = Value::Mapping(map);
964 assert!(schema.validate(&config).is_err());
965 }
966
967 #[test]
968 fn test_schema_from_json() {
969 let schema_json = r#"{
970 "type": "object",
971 "required": ["name"],
972 "properties": {
973 "name": { "type": "string" },
974 "port": { "type": "integer" }
975 }
976 }"#;
977 let schema = Schema::from_json(schema_json).unwrap();
978 assert!(schema.as_value().is_object());
979
980 let mut map = indexmap::IndexMap::new();
982 map.insert("name".into(), Value::String("test".into()));
983 let config = Value::Mapping(map);
984 assert!(schema.validate(&config).is_ok());
985 }
986
987 #[test]
988 fn test_schema_from_json_invalid() {
989 let invalid_json = "not valid json {{{";
990 let result = Schema::from_json(invalid_json);
991 assert!(result.is_err());
992 assert!(result
993 .unwrap_err()
994 .to_string()
995 .contains("Invalid JSON schema"));
996 }
997
998 #[test]
999 fn test_schema_from_yaml_invalid() {
1000 let invalid_yaml = ":: invalid yaml :::";
1001 let result = Schema::from_yaml(invalid_yaml);
1002 assert!(result.is_err());
1003 assert!(result
1004 .unwrap_err()
1005 .to_string()
1006 .contains("Invalid YAML schema"));
1007 }
1008
1009 #[test]
1010 fn test_schema_from_file_yaml() {
1011 let dir = std::env::temp_dir();
1012 let path = dir.join("test_schema.yaml");
1013
1014 let schema_content = r#"
1015type: object
1016properties:
1017 name:
1018 type: string
1019"#;
1020 std::fs::write(&path, schema_content).unwrap();
1021
1022 let schema = Schema::from_file(&path).unwrap();
1023 assert!(schema.as_value().is_object());
1024
1025 std::fs::remove_file(&path).ok();
1026 }
1027
1028 #[test]
1029 fn test_schema_from_file_json() {
1030 let dir = std::env::temp_dir();
1031 let path = dir.join("test_schema.json");
1032
1033 let schema_content = r#"{"type": "object", "properties": {"name": {"type": "string"}}}"#;
1034 std::fs::write(&path, schema_content).unwrap();
1035
1036 let schema = Schema::from_file(&path).unwrap();
1037 assert!(schema.as_value().is_object());
1038
1039 std::fs::remove_file(&path).ok();
1040 }
1041
1042 #[test]
1043 fn test_schema_from_file_yml_extension() {
1044 let dir = std::env::temp_dir();
1045 let path = dir.join("test_schema.yml");
1046
1047 let schema_content = r#"
1048type: object
1049properties:
1050 name:
1051 type: string
1052"#;
1053 std::fs::write(&path, schema_content).unwrap();
1054
1055 let schema = Schema::from_file(&path).unwrap();
1056 assert!(schema.as_value().is_object());
1057
1058 std::fs::remove_file(&path).ok();
1059 }
1060
1061 #[test]
1062 fn test_schema_from_file_no_extension() {
1063 let dir = std::env::temp_dir();
1064 let path = dir.join("test_schema_no_ext");
1065
1066 let schema_content = r#"
1068type: object
1069properties:
1070 name:
1071 type: string
1072"#;
1073 std::fs::write(&path, schema_content).unwrap();
1074
1075 let schema = Schema::from_file(&path).unwrap();
1076 assert!(schema.as_value().is_object());
1077
1078 std::fs::remove_file(&path).ok();
1079 }
1080
1081 #[test]
1082 fn test_schema_from_file_not_found() {
1083 let result = Schema::from_file("/nonexistent/path/to/schema.yaml");
1084 assert!(result.is_err());
1085 assert!(result.unwrap_err().to_string().contains("File not found"));
1086 }
1087
1088 #[test]
1089 fn test_schema_to_yaml() {
1090 let schema = Schema::from_yaml(
1091 r#"
1092type: object
1093properties:
1094 name:
1095 type: string
1096"#,
1097 )
1098 .unwrap();
1099
1100 let yaml = schema.to_yaml().unwrap();
1101 assert!(yaml.contains("type"));
1102 assert!(yaml.contains("object"));
1103 assert!(yaml.contains("properties"));
1104 }
1105
1106 #[test]
1107 fn test_schema_to_json() {
1108 let schema = Schema::from_yaml(
1109 r#"
1110type: object
1111properties:
1112 name:
1113 type: string
1114"#,
1115 )
1116 .unwrap();
1117
1118 let json = schema.to_json().unwrap();
1119 assert!(json.contains("\"type\""));
1120 assert!(json.contains("\"object\""));
1121 assert!(json.contains("\"properties\""));
1122 }
1123
1124 #[test]
1125 fn test_schema_to_markdown_basic() {
1126 let schema = Schema::from_yaml(
1127 r#"
1128title: Test Configuration
1129description: A test configuration schema
1130type: object
1131required:
1132 - name
1133properties:
1134 name:
1135 type: string
1136 description: The application name
1137 port:
1138 type: integer
1139 description: The server port
1140 default: 8080
1141 minimum: 1
1142 maximum: 65535
1143"#,
1144 )
1145 .unwrap();
1146
1147 let markdown = schema.to_markdown();
1148 assert!(markdown.contains("# Test Configuration"));
1149 assert!(markdown.contains("A test configuration schema"));
1150 assert!(markdown.contains("name"));
1151 assert!(markdown.contains("port"));
1152 assert!(markdown.contains("(required)"));
1153 }
1154
1155 #[test]
1156 fn test_schema_to_markdown_nested() {
1157 let schema = Schema::from_yaml(
1158 r#"
1159title: Nested Config
1160type: object
1161properties:
1162 database:
1163 type: object
1164 description: Database settings
1165 required:
1166 - host
1167 properties:
1168 host:
1169 type: string
1170 description: Database host
1171 port:
1172 type: integer
1173 default: 5432
1174"#,
1175 )
1176 .unwrap();
1177
1178 let markdown = schema.to_markdown();
1179 assert!(markdown.contains("database"));
1180 assert!(markdown.contains("host"));
1181 assert!(markdown.contains("5432"));
1182 }
1183
1184 #[test]
1185 fn test_schema_to_markdown_enum() {
1186 let schema = Schema::from_yaml(
1187 r#"
1188type: object
1189properties:
1190 log_level:
1191 type: string
1192 enum: [debug, info, warn, error]
1193"#,
1194 )
1195 .unwrap();
1196
1197 let markdown = schema.to_markdown();
1198 assert!(markdown.contains("enum:"));
1199 }
1200
1201 #[test]
1202 fn test_schema_to_markdown_constraints() {
1203 let schema = Schema::from_yaml(
1204 r#"
1205type: object
1206properties:
1207 name:
1208 type: string
1209 minLength: 1
1210 maxLength: 100
1211 pattern: "^[a-z]+$"
1212"#,
1213 )
1214 .unwrap();
1215
1216 let markdown = schema.to_markdown();
1217 assert!(markdown.contains("minLength"));
1218 assert!(markdown.contains("maxLength"));
1219 assert!(markdown.contains("pattern"));
1220 }
1221
1222 #[test]
1223 fn test_schema_to_template_basic() {
1224 let schema = Schema::from_yaml(
1225 r#"
1226title: Test Config
1227type: object
1228required:
1229 - name
1230properties:
1231 name:
1232 type: string
1233 description: The application name
1234 port:
1235 type: integer
1236 default: 8080
1237"#,
1238 )
1239 .unwrap();
1240
1241 let template = schema.to_template();
1242 assert!(template.contains("name:"));
1243 assert!(template.contains("port:"));
1244 assert!(template.contains("8080"));
1245 assert!(template.contains("REQUIRED"));
1246 }
1247
1248 #[test]
1249 fn test_schema_to_template_nested() {
1250 let schema = Schema::from_yaml(
1251 r#"
1252type: object
1253properties:
1254 database:
1255 type: object
1256 required:
1257 - host
1258 properties:
1259 host:
1260 type: string
1261 description: Database host
1262 port:
1263 type: integer
1264 default: 5432
1265"#,
1266 )
1267 .unwrap();
1268
1269 let template = schema.to_template();
1270 assert!(template.contains("database:"));
1271 assert!(template.contains("host:"));
1272 assert!(template.contains("port:"));
1273 assert!(template.contains("5432"));
1274 }
1275
1276 #[test]
1277 fn test_schema_to_template_types() {
1278 let schema = Schema::from_yaml(
1279 r#"
1280type: object
1281properties:
1282 string_field:
1283 type: string
1284 int_field:
1285 type: integer
1286 number_field:
1287 type: number
1288 bool_field:
1289 type: boolean
1290 array_field:
1291 type: array
1292 null_field:
1293 type: "null"
1294"#,
1295 )
1296 .unwrap();
1297
1298 let template = schema.to_template();
1299 assert!(template.contains("string_field: \"\""));
1300 assert!(template.contains("int_field: 0"));
1301 assert!(template.contains("number_field: 0.0"));
1302 assert!(template.contains("bool_field: false"));
1303 assert!(template.contains("array_field: []"));
1304 assert!(template.contains("null_field: null"));
1305 }
1306
1307 #[test]
1308 fn test_schema_to_template_enum() {
1309 let schema = Schema::from_yaml(
1310 r#"
1311type: object
1312properties:
1313 log_level:
1314 type: string
1315 enum: [debug, info, warn, error]
1316"#,
1317 )
1318 .unwrap();
1319
1320 let template = schema.to_template();
1321 assert!(template.contains("log_level: debug") || template.contains("log_level: \"debug\""));
1323 }
1324
1325 #[test]
1326 fn test_validation_error_display() {
1327 let err = ValidationError {
1328 path: "/database/port".to_string(),
1329 message: "expected integer".to_string(),
1330 };
1331 let display = format!("{}", err);
1332 assert_eq!(display, "/database/port: expected integer");
1333 }
1334
1335 #[test]
1336 fn test_validation_error_display_empty_path() {
1337 let err = ValidationError {
1338 path: "".to_string(),
1339 message: "missing required field".to_string(),
1340 };
1341 let display = format!("{}", err);
1342 assert_eq!(display, "missing required field");
1343 }
1344
1345 #[test]
1346 fn test_value_to_json_null() {
1347 let v = Value::Null;
1348 let json = value_to_json(&v);
1349 assert!(json.is_null());
1350 }
1351
1352 #[test]
1353 fn test_value_to_json_bool() {
1354 let v = Value::Bool(true);
1355 let json = value_to_json(&v);
1356 assert_eq!(json, serde_json::Value::Bool(true));
1357 }
1358
1359 #[test]
1360 fn test_value_to_json_integer() {
1361 let v = Value::Integer(42);
1362 let json = value_to_json(&v);
1363 assert_eq!(json, serde_json::json!(42));
1364 }
1365
1366 #[test]
1367 fn test_value_to_json_float() {
1368 let v = Value::Float(2.71);
1369 let json = value_to_json(&v);
1370 assert!(json.is_number());
1371 }
1372
1373 #[test]
1374 fn test_value_to_json_float_nan() {
1375 let v = Value::Float(f64::NAN);
1377 let json = value_to_json(&v);
1378 assert!(json.is_null());
1379 }
1380
1381 #[test]
1382 fn test_value_to_json_string() {
1383 let v = Value::String("hello".into());
1384 let json = value_to_json(&v);
1385 assert_eq!(json, serde_json::json!("hello"));
1386 }
1387
1388 #[test]
1389 fn test_value_to_json_bytes() {
1390 let v = Value::Bytes(vec![72, 101, 108, 108, 111]); let json = value_to_json(&v);
1392 assert!(json.is_string());
1394 assert_eq!(json.as_str().unwrap(), "SGVsbG8=");
1395 }
1396
1397 #[test]
1398 fn test_value_to_json_sequence() {
1399 let v = Value::Sequence(vec![Value::Integer(1), Value::Integer(2)]);
1400 let json = value_to_json(&v);
1401 assert!(json.is_array());
1402 assert_eq!(json, serde_json::json!([1, 2]));
1403 }
1404
1405 #[test]
1406 fn test_value_to_json_mapping() {
1407 let mut map = indexmap::IndexMap::new();
1408 map.insert("key".to_string(), Value::String("value".into()));
1409 let v = Value::Mapping(map);
1410 let json = value_to_json(&v);
1411 assert!(json.is_object());
1412 assert_eq!(json["key"], "value");
1413 }
1414
1415 #[test]
1416 fn test_format_json_value_null() {
1417 let v = serde_json::Value::Null;
1418 assert_eq!(format_json_value(&v), "null");
1419 }
1420
1421 #[test]
1422 fn test_format_json_value_bool() {
1423 assert_eq!(format_json_value(&serde_json::json!(true)), "true");
1424 assert_eq!(format_json_value(&serde_json::json!(false)), "false");
1425 }
1426
1427 #[test]
1428 fn test_format_json_value_number() {
1429 assert_eq!(format_json_value(&serde_json::json!(42)), "42");
1430 assert_eq!(format_json_value(&serde_json::json!(2.71)), "2.71");
1431 }
1432
1433 #[test]
1434 fn test_format_json_value_string_simple() {
1435 assert_eq!(format_json_value(&serde_json::json!("hello")), "hello");
1436 }
1437
1438 #[test]
1439 fn test_format_json_value_string_needs_quoting() {
1440 assert_eq!(format_json_value(&serde_json::json!("")), "\"\"");
1442 assert_eq!(
1444 format_json_value(&serde_json::json!("key:value")),
1445 "\"key:value\""
1446 );
1447 assert_eq!(
1449 format_json_value(&serde_json::json!("has#comment")),
1450 "\"has#comment\""
1451 );
1452 assert_eq!(
1454 format_json_value(&serde_json::json!(" leading")),
1455 "\" leading\""
1456 );
1457 assert_eq!(
1459 format_json_value(&serde_json::json!("trailing ")),
1460 "\"trailing \""
1461 );
1462 }
1463
1464 #[test]
1465 fn test_format_json_value_string_with_quotes_needing_escape() {
1466 let v = serde_json::Value::String("has:\"quotes\"".to_string());
1469 let formatted = format_json_value(&v);
1470 assert!(formatted.contains("\\\""));
1472 assert!(formatted.starts_with('"'));
1473 }
1474
1475 #[test]
1476 fn test_format_json_value_string_no_quoting_needed() {
1477 let v = serde_json::Value::String("has \"quotes\"".to_string());
1479 let formatted = format_json_value(&v);
1480 assert_eq!(formatted, "has \"quotes\"");
1482 }
1483
1484 #[test]
1485 fn test_format_json_value_array_empty() {
1486 assert_eq!(format_json_value(&serde_json::json!([])), "[]");
1487 }
1488
1489 #[test]
1490 fn test_format_json_value_array_with_items() {
1491 assert_eq!(
1492 format_json_value(&serde_json::json!([1, 2, 3])),
1493 "[1, 2, 3]"
1494 );
1495 }
1496
1497 #[test]
1498 fn test_format_json_value_object() {
1499 assert_eq!(format_json_value(&serde_json::json!({})), "{}");
1500 }
1501
1502 #[test]
1503 fn test_get_type_string_basic() {
1504 let schema = serde_json::json!({"type": "string"});
1505 assert_eq!(get_type_string(&schema), "string");
1506 }
1507
1508 #[test]
1509 fn test_get_type_string_with_constraints() {
1510 let schema = serde_json::json!({
1511 "type": "integer",
1512 "minimum": 1,
1513 "maximum": 100
1514 });
1515 let type_str = get_type_string(&schema);
1516 assert!(type_str.contains("integer"));
1517 assert!(type_str.contains("min: 1"));
1518 assert!(type_str.contains("max: 100"));
1519 }
1520
1521 #[test]
1522 fn test_get_type_string_with_string_constraints() {
1523 let schema = serde_json::json!({
1524 "type": "string",
1525 "minLength": 1,
1526 "maxLength": 50,
1527 "pattern": "^[a-z]+$"
1528 });
1529 let type_str = get_type_string(&schema);
1530 assert!(type_str.contains("string"));
1531 assert!(type_str.contains("minLength: 1"));
1532 assert!(type_str.contains("maxLength: 50"));
1533 assert!(type_str.contains("pattern:"));
1534 }
1535
1536 #[test]
1537 fn test_get_type_string_enum() {
1538 let schema = serde_json::json!({
1539 "enum": ["a", "b", "c"]
1540 });
1541 let type_str = get_type_string(&schema);
1542 assert!(type_str.starts_with("enum:"));
1543 assert!(type_str.contains("\"a\""));
1544 assert!(type_str.contains("\"b\""));
1545 }
1546
1547 #[test]
1548 fn test_get_type_string_enum_numeric() {
1549 let schema = serde_json::json!({
1550 "enum": [1, 2, 3]
1551 });
1552 let type_str = get_type_string(&schema);
1553 assert!(type_str.contains("1"));
1554 assert!(type_str.contains("2"));
1555 }
1556
1557 #[test]
1558 fn test_get_type_string_no_type() {
1559 let schema = serde_json::json!({});
1560 assert_eq!(get_type_string(&schema), "any");
1561 }
1562
1563 #[test]
1564 fn test_schema_default_string_with_default() {
1565 let schema = serde_json::json!({"default": 42});
1566 assert_eq!(schema_default_string(&schema), "42");
1567 }
1568
1569 #[test]
1570 fn test_schema_default_string_with_string_default() {
1571 let schema = serde_json::json!({"default": "hello"});
1572 assert_eq!(schema_default_string(&schema), "\"hello\"");
1573 }
1574
1575 #[test]
1576 fn test_schema_default_string_no_default() {
1577 let schema = serde_json::json!({});
1578 assert_eq!(schema_default_string(&schema), "-");
1579 }
1580
1581 #[test]
1582 fn test_get_template_value_with_default() {
1583 let schema = serde_json::json!({"type": "string", "default": "myvalue"});
1584 assert_eq!(get_template_value(&schema, "string"), "myvalue");
1585 }
1586
1587 #[test]
1588 fn test_get_template_value_with_enum() {
1589 let schema = serde_json::json!({"type": "string", "enum": ["first", "second"]});
1590 assert_eq!(get_template_value(&schema, "string"), "first");
1591 }
1592
1593 #[test]
1594 fn test_get_template_value_placeholders() {
1595 assert_eq!(get_template_value(&serde_json::json!({}), "string"), "\"\"");
1596 assert_eq!(get_template_value(&serde_json::json!({}), "integer"), "0");
1597 assert_eq!(get_template_value(&serde_json::json!({}), "number"), "0.0");
1598 assert_eq!(
1599 get_template_value(&serde_json::json!({}), "boolean"),
1600 "false"
1601 );
1602 assert_eq!(get_template_value(&serde_json::json!({}), "array"), "[]");
1603 assert_eq!(get_template_value(&serde_json::json!({}), "null"), "null");
1604 assert_eq!(
1605 get_template_value(&serde_json::json!({}), "unknown"),
1606 "null"
1607 );
1608 }
1609
1610 #[test]
1611 fn test_schema_to_markdown_no_title() {
1612 let schema = Schema::from_yaml(
1614 r#"
1615type: object
1616properties:
1617 name:
1618 type: string
1619"#,
1620 )
1621 .unwrap();
1622
1623 let markdown = schema.to_markdown();
1624 assert!(markdown.contains("# Configuration Reference"));
1625 }
1626
1627 #[test]
1628 fn test_schema_to_markdown_non_object_property() {
1629 let schema = Schema::from_yaml(
1631 r#"
1632type: object
1633required:
1634 - port
1635properties:
1636 port:
1637 type: integer
1638 description: Server port
1639"#,
1640 )
1641 .unwrap();
1642
1643 let markdown = schema.to_markdown();
1644 assert!(markdown.contains("port"));
1645 assert!(markdown.contains("(required)"));
1646 }
1647
1648 #[test]
1649 fn test_schema_to_template_no_title() {
1650 let schema = Schema::from_yaml(
1651 r#"
1652type: object
1653properties:
1654 name:
1655 type: string
1656"#,
1657 )
1658 .unwrap();
1659
1660 let template = schema.to_template();
1661 assert!(template.contains("Configuration template generated from schema"));
1662 }
1663
1664 #[test]
1665 fn test_schema_to_template_with_description() {
1666 let schema = Schema::from_yaml(
1667 r#"
1668type: object
1669properties:
1670 name:
1671 type: string
1672 description: The name field
1673"#,
1674 )
1675 .unwrap();
1676
1677 let template = schema.to_template();
1678 assert!(template.contains("The name field"));
1679 }
1680
1681 #[test]
1682 fn test_schema_to_template_with_default_and_description() {
1683 let schema = Schema::from_yaml(
1684 r#"
1685type: object
1686properties:
1687 port:
1688 type: integer
1689 description: Server port
1690 default: 8080
1691"#,
1692 )
1693 .unwrap();
1694
1695 let template = schema.to_template();
1696 assert!(template.contains("8080"));
1697 assert!(template.contains("default:"));
1698 }
1699
1700 #[test]
1703 fn test_get_default_simple() {
1704 let schema = Schema::from_yaml(
1705 r#"
1706type: object
1707properties:
1708 pool_size:
1709 type: integer
1710 default: 10
1711 timeout:
1712 type: number
1713 default: 30.5
1714 enabled:
1715 type: boolean
1716 default: true
1717 name:
1718 type: string
1719 default: "default_name"
1720"#,
1721 )
1722 .unwrap();
1723
1724 assert_eq!(schema.get_default("pool_size"), Some(Value::Integer(10)));
1725 assert_eq!(schema.get_default("timeout"), Some(Value::Float(30.5)));
1726 assert_eq!(schema.get_default("enabled"), Some(Value::Bool(true)));
1727 assert_eq!(
1728 schema.get_default("name"),
1729 Some(Value::String("default_name".into()))
1730 );
1731 assert_eq!(schema.get_default("nonexistent"), None);
1732 }
1733
1734 #[test]
1735 fn test_get_default_nested() {
1736 let schema = Schema::from_yaml(
1737 r#"
1738type: object
1739properties:
1740 database:
1741 type: object
1742 properties:
1743 host:
1744 type: string
1745 default: localhost
1746 port:
1747 type: integer
1748 default: 5432
1749 pool:
1750 type: object
1751 properties:
1752 size:
1753 type: integer
1754 default: 10
1755"#,
1756 )
1757 .unwrap();
1758
1759 assert_eq!(
1760 schema.get_default("database.host"),
1761 Some(Value::String("localhost".into()))
1762 );
1763 assert_eq!(
1764 schema.get_default("database.port"),
1765 Some(Value::Integer(5432))
1766 );
1767 assert_eq!(
1768 schema.get_default("database.pool.size"),
1769 Some(Value::Integer(10))
1770 );
1771 assert_eq!(schema.get_default("database.nonexistent"), None);
1772 }
1773
1774 #[test]
1775 fn test_get_default_object_level() {
1776 let schema = Schema::from_yaml(
1777 r#"
1778type: object
1779properties:
1780 logging:
1781 type: object
1782 default:
1783 level: info
1784 format: json
1785"#,
1786 )
1787 .unwrap();
1788
1789 let default = schema.get_default("logging").unwrap();
1790 match default {
1791 Value::Mapping(map) => {
1792 assert_eq!(map.get("level"), Some(&Value::String("info".into())));
1793 assert_eq!(map.get("format"), Some(&Value::String("json".into())));
1794 }
1795 _ => panic!("Expected mapping default"),
1796 }
1797 }
1798
1799 #[test]
1800 fn test_get_default_null_default() {
1801 let schema = Schema::from_yaml(
1802 r#"
1803type: object
1804properties:
1805 optional_value:
1806 type:
1807 - string
1808 - "null"
1809 default: null
1810"#,
1811 )
1812 .unwrap();
1813
1814 assert_eq!(schema.get_default("optional_value"), Some(Value::Null));
1815 }
1816
1817 #[test]
1818 fn test_allows_null_single_type() {
1819 let schema = Schema::from_yaml(
1820 r#"
1821type: object
1822properties:
1823 required_string:
1824 type: string
1825 nullable_string:
1826 type: "null"
1827"#,
1828 )
1829 .unwrap();
1830
1831 assert!(!schema.allows_null("required_string"));
1832 assert!(schema.allows_null("nullable_string"));
1833 }
1834
1835 #[test]
1836 fn test_allows_null_array_type() {
1837 let schema = Schema::from_yaml(
1838 r#"
1839type: object
1840properties:
1841 nullable_value:
1842 type:
1843 - string
1844 - "null"
1845 non_nullable:
1846 type:
1847 - string
1848 - integer
1849"#,
1850 )
1851 .unwrap();
1852
1853 assert!(schema.allows_null("nullable_value"));
1854 assert!(!schema.allows_null("non_nullable"));
1855 }
1856
1857 #[test]
1858 fn test_allows_null_nested() {
1859 let schema = Schema::from_yaml(
1860 r#"
1861type: object
1862properties:
1863 database:
1864 type: object
1865 properties:
1866 connection_string:
1867 type:
1868 - string
1869 - "null"
1870 default: null
1871"#,
1872 )
1873 .unwrap();
1874
1875 assert!(schema.allows_null("database.connection_string"));
1876 }
1877
1878 #[test]
1879 fn test_allows_null_no_type_specified() {
1880 let schema = Schema::from_yaml(
1882 r#"
1883type: object
1884properties:
1885 any_value: {}
1886"#,
1887 )
1888 .unwrap();
1889
1890 assert!(schema.allows_null("any_value"));
1891 }
1892}