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::Stream(_) => {
499 panic!("Value::Stream should be materialized before schema validation")
502 }
503 Value::Sequence(seq) => serde_json::Value::Array(seq.iter().map(value_to_json).collect()),
504 Value::Mapping(map) => {
505 let obj: serde_json::Map<String, serde_json::Value> = map
506 .iter()
507 .map(|(k, v)| (k.clone(), value_to_json(v)))
508 .collect();
509 serde_json::Value::Object(obj)
510 }
511 }
512}
513
514fn generate_template(schema: &serde_json::Value) -> String {
516 let mut output = String::new();
517
518 if let Some(title) = schema.get("title").and_then(|v| v.as_str()) {
520 output.push_str(&format!("# Generated from: {}\n", title));
521 } else {
522 output.push_str("# Configuration template generated from schema\n");
523 }
524 output.push_str("# Required fields marked with # REQUIRED\n\n");
525
526 let root_required: Vec<&str> = schema
528 .get("required")
529 .and_then(|v| v.as_array())
530 .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
531 .unwrap_or_default();
532
533 if let Some(properties) = schema.get("properties").and_then(|v| v.as_object()) {
535 for (name, prop_schema) in properties {
536 let is_required = root_required.contains(&name.as_str());
537 generate_template_property(&mut output, name, prop_schema, is_required, 0);
538 }
539 }
540
541 output
542}
543
544fn generate_template_property(
546 output: &mut String,
547 name: &str,
548 schema: &serde_json::Value,
549 is_required: bool,
550 indent_level: usize,
551) {
552 let indent = " ".repeat(indent_level);
553
554 let mut comment_parts = Vec::new();
556 if is_required {
557 comment_parts.push("REQUIRED".to_string());
558 }
559 if let Some(desc) = schema.get("description").and_then(|v| v.as_str()) {
560 comment_parts.push(desc.to_string());
561 }
562
563 let prop_type = schema.get("type").and_then(|v| v.as_str()).unwrap_or("any");
565
566 if prop_type == "object" {
568 if !comment_parts.is_empty() {
570 output.push_str(&format!("{}# {}\n", indent, comment_parts.join(" - ")));
571 }
572 output.push_str(&format!("{}{}:\n", indent, name));
573
574 let required: Vec<&str> = schema
576 .get("required")
577 .and_then(|v| v.as_array())
578 .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
579 .unwrap_or_default();
580
581 if let Some(properties) = schema.get("properties").and_then(|v| v.as_object()) {
583 for (prop_name, prop_schema) in properties {
584 let prop_required = required.contains(&prop_name.as_str());
585 generate_template_property(
586 output,
587 prop_name,
588 prop_schema,
589 prop_required,
590 indent_level + 1,
591 );
592 }
593 }
594 } else {
595 let value_str = get_template_value(schema, prop_type);
597
598 let mut line = format!("{}{}: {}", indent, name, value_str);
600
601 if let Some(default) = schema.get("default") {
603 if !comment_parts.is_empty() {
604 line.push_str(&format!(
605 " # {} (default: {})",
606 comment_parts.join(" - "),
607 format_json_value(default)
608 ));
609 } else {
610 line.push_str(&format!(" # default: {}", format_json_value(default)));
611 }
612 } else if !comment_parts.is_empty() {
613 line.push_str(&format!(" # {}", comment_parts.join(" - ")));
614 }
615
616 output.push_str(&line);
617 output.push('\n');
618 }
619}
620
621fn get_template_value(schema: &serde_json::Value, prop_type: &str) -> String {
623 if let Some(default) = schema.get("default") {
625 return format_json_value(default);
626 }
627
628 if let Some(enum_vals) = schema.get("enum").and_then(|v| v.as_array()) {
630 if let Some(first) = enum_vals.first() {
631 return format_json_value(first);
632 }
633 }
634
635 match prop_type {
637 "string" => "\"\"".to_string(),
638 "integer" => "0".to_string(),
639 "number" => "0.0".to_string(),
640 "boolean" => "false".to_string(),
641 "array" => "[]".to_string(),
642 "null" => "null".to_string(),
643 _ => "null".to_string(),
644 }
645}
646
647fn format_json_value(value: &serde_json::Value) -> String {
649 match value {
650 serde_json::Value::Null => "null".to_string(),
651 serde_json::Value::Bool(b) => b.to_string(),
652 serde_json::Value::Number(n) => n.to_string(),
653 serde_json::Value::String(s) => {
654 if s.is_empty()
656 || s.contains(':')
657 || s.contains('#')
658 || s.starts_with(' ')
659 || s.ends_with(' ')
660 {
661 format!("\"{}\"", s.replace('"', "\\\""))
662 } else {
663 s.clone()
664 }
665 }
666 serde_json::Value::Array(arr) => {
667 if arr.is_empty() {
668 "[]".to_string()
669 } else {
670 let items: Vec<String> = arr.iter().map(format_json_value).collect();
672 format!("[{}]", items.join(", "))
673 }
674 }
675 serde_json::Value::Object(_) => "{}".to_string(),
676 }
677}
678
679#[cfg(test)]
680mod tests {
681 use super::*;
682
683 #[test]
684 fn test_schema_from_yaml() {
685 let schema_yaml = r#"
686type: object
687required:
688 - name
689properties:
690 name:
691 type: string
692 port:
693 type: integer
694 minimum: 1
695 maximum: 65535
696"#;
697 let schema = Schema::from_yaml(schema_yaml).unwrap();
698 assert!(schema.as_value().is_object());
699 }
700
701 #[test]
702 fn test_validate_valid_config() {
703 let schema = Schema::from_yaml(
704 r#"
705type: object
706properties:
707 name:
708 type: string
709 port:
710 type: integer
711"#,
712 )
713 .unwrap();
714
715 let mut map = indexmap::IndexMap::new();
716 map.insert("name".into(), Value::String("myapp".into()));
717 map.insert("port".into(), Value::Integer(8080));
718 let config = Value::Mapping(map);
719
720 assert!(schema.validate(&config).is_ok());
721 }
722
723 #[test]
724 fn test_validate_missing_required() {
725 let schema = Schema::from_yaml(
726 r#"
727type: object
728required:
729 - name
730properties:
731 name:
732 type: string
733"#,
734 )
735 .unwrap();
736
737 let config = Value::Mapping(indexmap::IndexMap::new());
738 let result = schema.validate(&config);
739 assert!(result.is_err());
740 let err = result.unwrap_err();
741 assert!(err.to_string().contains("name"));
742 }
743
744 #[test]
745 fn test_validate_wrong_type() {
746 let schema = Schema::from_yaml(
747 r#"
748type: object
749properties:
750 port:
751 type: integer
752"#,
753 )
754 .unwrap();
755
756 let mut map = indexmap::IndexMap::new();
757 map.insert("port".into(), Value::String("not-a-number".into()));
758 let config = Value::Mapping(map);
759
760 let result = schema.validate(&config);
761 assert!(result.is_err());
762 }
763
764 #[test]
765 fn test_validate_constraint_violation() {
766 let schema = Schema::from_yaml(
767 r#"
768type: object
769properties:
770 port:
771 type: integer
772 minimum: 1
773 maximum: 65535
774"#,
775 )
776 .unwrap();
777
778 let mut map = indexmap::IndexMap::new();
779 map.insert("port".into(), Value::Integer(70000));
780 let config = Value::Mapping(map);
781
782 let result = schema.validate(&config);
783 assert!(result.is_err());
784 }
785
786 #[test]
787 fn test_validate_enum() {
788 let schema = Schema::from_yaml(
789 r#"
790type: object
791properties:
792 log_level:
793 type: string
794 enum: [debug, info, warn, error]
795"#,
796 )
797 .unwrap();
798
799 let mut map = indexmap::IndexMap::new();
801 map.insert("log_level".into(), Value::String("info".into()));
802 let config = Value::Mapping(map);
803 assert!(schema.validate(&config).is_ok());
804
805 let mut map = indexmap::IndexMap::new();
807 map.insert("log_level".into(), Value::String("verbose".into()));
808 let config = Value::Mapping(map);
809 assert!(schema.validate(&config).is_err());
810 }
811
812 #[test]
813 fn test_validate_nested() {
814 let schema = Schema::from_yaml(
815 r#"
816type: object
817properties:
818 database:
819 type: object
820 required: [host]
821 properties:
822 host:
823 type: string
824 port:
825 type: integer
826 default: 5432
827"#,
828 )
829 .unwrap();
830
831 let mut db = indexmap::IndexMap::new();
833 db.insert("host".into(), Value::String("localhost".into()));
834 db.insert("port".into(), Value::Integer(5432));
835 let mut map = indexmap::IndexMap::new();
836 map.insert("database".into(), Value::Mapping(db));
837 let config = Value::Mapping(map);
838 assert!(schema.validate(&config).is_ok());
839
840 let db = indexmap::IndexMap::new();
842 let mut map = indexmap::IndexMap::new();
843 map.insert("database".into(), Value::Mapping(db));
844 let config = Value::Mapping(map);
845 assert!(schema.validate(&config).is_err());
846 }
847
848 #[test]
849 fn test_validate_collect_multiple_errors() {
850 let schema = Schema::from_yaml(
851 r#"
852type: object
853required:
854 - name
855 - port
856properties:
857 name:
858 type: string
859 port:
860 type: integer
861"#,
862 )
863 .unwrap();
864
865 let config = Value::Mapping(indexmap::IndexMap::new());
866 let errors = schema.validate_collect(&config);
867 assert!(!errors.is_empty());
869 }
870
871 #[test]
872 fn test_validate_additional_properties_allowed() {
873 let schema = Schema::from_yaml(
875 r#"
876type: object
877properties:
878 name:
879 type: string
880"#,
881 )
882 .unwrap();
883
884 let mut map = indexmap::IndexMap::new();
885 map.insert("name".into(), Value::String("myapp".into()));
886 map.insert("extra".into(), Value::String("allowed".into()));
887 let config = Value::Mapping(map);
888 assert!(schema.validate(&config).is_ok());
889 }
890
891 #[test]
892 fn test_validate_additional_properties_denied() {
893 let schema = Schema::from_yaml(
894 r#"
895type: object
896properties:
897 name:
898 type: string
899additionalProperties: false
900"#,
901 )
902 .unwrap();
903
904 let mut map = indexmap::IndexMap::new();
905 map.insert("name".into(), Value::String("myapp".into()));
906 map.insert("extra".into(), Value::String("not allowed".into()));
907 let config = Value::Mapping(map);
908 assert!(schema.validate(&config).is_err());
909 }
910
911 #[test]
912 fn test_validate_array() {
913 let schema = Schema::from_yaml(
914 r#"
915type: object
916properties:
917 servers:
918 type: array
919 items:
920 type: string
921"#,
922 )
923 .unwrap();
924
925 let mut map = indexmap::IndexMap::new();
926 map.insert(
927 "servers".into(),
928 Value::Sequence(vec![
929 Value::String("server1".into()),
930 Value::String("server2".into()),
931 ]),
932 );
933 let config = Value::Mapping(map);
934 assert!(schema.validate(&config).is_ok());
935
936 let mut map = indexmap::IndexMap::new();
938 map.insert(
939 "servers".into(),
940 Value::Sequence(vec![Value::String("server1".into()), Value::Integer(123)]),
941 );
942 let config = Value::Mapping(map);
943 assert!(schema.validate(&config).is_err());
944 }
945
946 #[test]
947 fn test_validate_pattern() {
948 let schema = Schema::from_yaml(
949 r#"
950type: object
951properties:
952 version:
953 type: string
954 pattern: "^\\d+\\.\\d+\\.\\d+$"
955"#,
956 )
957 .unwrap();
958
959 let mut map = indexmap::IndexMap::new();
961 map.insert("version".into(), Value::String("1.2.3".into()));
962 let config = Value::Mapping(map);
963 assert!(schema.validate(&config).is_ok());
964
965 let mut map = indexmap::IndexMap::new();
967 map.insert("version".into(), Value::String("v1.2".into()));
968 let config = Value::Mapping(map);
969 assert!(schema.validate(&config).is_err());
970 }
971
972 #[test]
973 fn test_schema_from_json() {
974 let schema_json = r#"{
975 "type": "object",
976 "required": ["name"],
977 "properties": {
978 "name": { "type": "string" },
979 "port": { "type": "integer" }
980 }
981 }"#;
982 let schema = Schema::from_json(schema_json).unwrap();
983 assert!(schema.as_value().is_object());
984
985 let mut map = indexmap::IndexMap::new();
987 map.insert("name".into(), Value::String("test".into()));
988 let config = Value::Mapping(map);
989 assert!(schema.validate(&config).is_ok());
990 }
991
992 #[test]
993 fn test_schema_from_json_invalid() {
994 let invalid_json = "not valid json {{{";
995 let result = Schema::from_json(invalid_json);
996 assert!(result.is_err());
997 assert!(result
998 .unwrap_err()
999 .to_string()
1000 .contains("Invalid JSON schema"));
1001 }
1002
1003 #[test]
1004 fn test_schema_from_yaml_invalid() {
1005 let invalid_yaml = ":: invalid yaml :::";
1006 let result = Schema::from_yaml(invalid_yaml);
1007 assert!(result.is_err());
1008 assert!(result
1009 .unwrap_err()
1010 .to_string()
1011 .contains("Invalid YAML schema"));
1012 }
1013
1014 #[test]
1015 fn test_schema_from_file_yaml() {
1016 let dir = std::env::temp_dir();
1017 let path = dir.join("test_schema.yaml");
1018
1019 let schema_content = r#"
1020type: object
1021properties:
1022 name:
1023 type: string
1024"#;
1025 std::fs::write(&path, schema_content).unwrap();
1026
1027 let schema = Schema::from_file(&path).unwrap();
1028 assert!(schema.as_value().is_object());
1029
1030 std::fs::remove_file(&path).ok();
1031 }
1032
1033 #[test]
1034 fn test_schema_from_file_json() {
1035 let dir = std::env::temp_dir();
1036 let path = dir.join("test_schema.json");
1037
1038 let schema_content = r#"{"type": "object", "properties": {"name": {"type": "string"}}}"#;
1039 std::fs::write(&path, schema_content).unwrap();
1040
1041 let schema = Schema::from_file(&path).unwrap();
1042 assert!(schema.as_value().is_object());
1043
1044 std::fs::remove_file(&path).ok();
1045 }
1046
1047 #[test]
1048 fn test_schema_from_file_yml_extension() {
1049 let dir = std::env::temp_dir();
1050 let path = dir.join("test_schema.yml");
1051
1052 let schema_content = r#"
1053type: object
1054properties:
1055 name:
1056 type: string
1057"#;
1058 std::fs::write(&path, schema_content).unwrap();
1059
1060 let schema = Schema::from_file(&path).unwrap();
1061 assert!(schema.as_value().is_object());
1062
1063 std::fs::remove_file(&path).ok();
1064 }
1065
1066 #[test]
1067 fn test_schema_from_file_no_extension() {
1068 let dir = std::env::temp_dir();
1069 let path = dir.join("test_schema_no_ext");
1070
1071 let schema_content = r#"
1073type: object
1074properties:
1075 name:
1076 type: string
1077"#;
1078 std::fs::write(&path, schema_content).unwrap();
1079
1080 let schema = Schema::from_file(&path).unwrap();
1081 assert!(schema.as_value().is_object());
1082
1083 std::fs::remove_file(&path).ok();
1084 }
1085
1086 #[test]
1087 fn test_schema_from_file_not_found() {
1088 let result = Schema::from_file("/nonexistent/path/to/schema.yaml");
1089 assert!(result.is_err());
1090 assert!(result.unwrap_err().to_string().contains("File not found"));
1091 }
1092
1093 #[test]
1094 fn test_schema_to_yaml() {
1095 let schema = Schema::from_yaml(
1096 r#"
1097type: object
1098properties:
1099 name:
1100 type: string
1101"#,
1102 )
1103 .unwrap();
1104
1105 let yaml = schema.to_yaml().unwrap();
1106 assert!(yaml.contains("type"));
1107 assert!(yaml.contains("object"));
1108 assert!(yaml.contains("properties"));
1109 }
1110
1111 #[test]
1112 fn test_schema_to_json() {
1113 let schema = Schema::from_yaml(
1114 r#"
1115type: object
1116properties:
1117 name:
1118 type: string
1119"#,
1120 )
1121 .unwrap();
1122
1123 let json = schema.to_json().unwrap();
1124 assert!(json.contains("\"type\""));
1125 assert!(json.contains("\"object\""));
1126 assert!(json.contains("\"properties\""));
1127 }
1128
1129 #[test]
1130 fn test_schema_to_markdown_basic() {
1131 let schema = Schema::from_yaml(
1132 r#"
1133title: Test Configuration
1134description: A test configuration schema
1135type: object
1136required:
1137 - name
1138properties:
1139 name:
1140 type: string
1141 description: The application name
1142 port:
1143 type: integer
1144 description: The server port
1145 default: 8080
1146 minimum: 1
1147 maximum: 65535
1148"#,
1149 )
1150 .unwrap();
1151
1152 let markdown = schema.to_markdown();
1153 assert!(markdown.contains("# Test Configuration"));
1154 assert!(markdown.contains("A test configuration schema"));
1155 assert!(markdown.contains("name"));
1156 assert!(markdown.contains("port"));
1157 assert!(markdown.contains("(required)"));
1158 }
1159
1160 #[test]
1161 fn test_schema_to_markdown_nested() {
1162 let schema = Schema::from_yaml(
1163 r#"
1164title: Nested Config
1165type: object
1166properties:
1167 database:
1168 type: object
1169 description: Database settings
1170 required:
1171 - host
1172 properties:
1173 host:
1174 type: string
1175 description: Database host
1176 port:
1177 type: integer
1178 default: 5432
1179"#,
1180 )
1181 .unwrap();
1182
1183 let markdown = schema.to_markdown();
1184 assert!(markdown.contains("database"));
1185 assert!(markdown.contains("host"));
1186 assert!(markdown.contains("5432"));
1187 }
1188
1189 #[test]
1190 fn test_schema_to_markdown_enum() {
1191 let schema = Schema::from_yaml(
1192 r#"
1193type: object
1194properties:
1195 log_level:
1196 type: string
1197 enum: [debug, info, warn, error]
1198"#,
1199 )
1200 .unwrap();
1201
1202 let markdown = schema.to_markdown();
1203 assert!(markdown.contains("enum:"));
1204 }
1205
1206 #[test]
1207 fn test_schema_to_markdown_constraints() {
1208 let schema = Schema::from_yaml(
1209 r#"
1210type: object
1211properties:
1212 name:
1213 type: string
1214 minLength: 1
1215 maxLength: 100
1216 pattern: "^[a-z]+$"
1217"#,
1218 )
1219 .unwrap();
1220
1221 let markdown = schema.to_markdown();
1222 assert!(markdown.contains("minLength"));
1223 assert!(markdown.contains("maxLength"));
1224 assert!(markdown.contains("pattern"));
1225 }
1226
1227 #[test]
1228 fn test_schema_to_template_basic() {
1229 let schema = Schema::from_yaml(
1230 r#"
1231title: Test Config
1232type: object
1233required:
1234 - name
1235properties:
1236 name:
1237 type: string
1238 description: The application name
1239 port:
1240 type: integer
1241 default: 8080
1242"#,
1243 )
1244 .unwrap();
1245
1246 let template = schema.to_template();
1247 assert!(template.contains("name:"));
1248 assert!(template.contains("port:"));
1249 assert!(template.contains("8080"));
1250 assert!(template.contains("REQUIRED"));
1251 }
1252
1253 #[test]
1254 fn test_schema_to_template_nested() {
1255 let schema = Schema::from_yaml(
1256 r#"
1257type: object
1258properties:
1259 database:
1260 type: object
1261 required:
1262 - host
1263 properties:
1264 host:
1265 type: string
1266 description: Database host
1267 port:
1268 type: integer
1269 default: 5432
1270"#,
1271 )
1272 .unwrap();
1273
1274 let template = schema.to_template();
1275 assert!(template.contains("database:"));
1276 assert!(template.contains("host:"));
1277 assert!(template.contains("port:"));
1278 assert!(template.contains("5432"));
1279 }
1280
1281 #[test]
1282 fn test_schema_to_template_types() {
1283 let schema = Schema::from_yaml(
1284 r#"
1285type: object
1286properties:
1287 string_field:
1288 type: string
1289 int_field:
1290 type: integer
1291 number_field:
1292 type: number
1293 bool_field:
1294 type: boolean
1295 array_field:
1296 type: array
1297 null_field:
1298 type: "null"
1299"#,
1300 )
1301 .unwrap();
1302
1303 let template = schema.to_template();
1304 assert!(template.contains("string_field: \"\""));
1305 assert!(template.contains("int_field: 0"));
1306 assert!(template.contains("number_field: 0.0"));
1307 assert!(template.contains("bool_field: false"));
1308 assert!(template.contains("array_field: []"));
1309 assert!(template.contains("null_field: null"));
1310 }
1311
1312 #[test]
1313 fn test_schema_to_template_enum() {
1314 let schema = Schema::from_yaml(
1315 r#"
1316type: object
1317properties:
1318 log_level:
1319 type: string
1320 enum: [debug, info, warn, error]
1321"#,
1322 )
1323 .unwrap();
1324
1325 let template = schema.to_template();
1326 assert!(template.contains("log_level: debug") || template.contains("log_level: \"debug\""));
1328 }
1329
1330 #[test]
1331 fn test_validation_error_display() {
1332 let err = ValidationError {
1333 path: "/database/port".to_string(),
1334 message: "expected integer".to_string(),
1335 };
1336 let display = format!("{}", err);
1337 assert_eq!(display, "/database/port: expected integer");
1338 }
1339
1340 #[test]
1341 fn test_validation_error_display_empty_path() {
1342 let err = ValidationError {
1343 path: "".to_string(),
1344 message: "missing required field".to_string(),
1345 };
1346 let display = format!("{}", err);
1347 assert_eq!(display, "missing required field");
1348 }
1349
1350 #[test]
1351 fn test_value_to_json_null() {
1352 let v = Value::Null;
1353 let json = value_to_json(&v);
1354 assert!(json.is_null());
1355 }
1356
1357 #[test]
1358 fn test_value_to_json_bool() {
1359 let v = Value::Bool(true);
1360 let json = value_to_json(&v);
1361 assert_eq!(json, serde_json::Value::Bool(true));
1362 }
1363
1364 #[test]
1365 fn test_value_to_json_integer() {
1366 let v = Value::Integer(42);
1367 let json = value_to_json(&v);
1368 assert_eq!(json, serde_json::json!(42));
1369 }
1370
1371 #[test]
1372 fn test_value_to_json_float() {
1373 let v = Value::Float(2.71);
1374 let json = value_to_json(&v);
1375 assert!(json.is_number());
1376 }
1377
1378 #[test]
1379 fn test_value_to_json_float_nan() {
1380 let v = Value::Float(f64::NAN);
1382 let json = value_to_json(&v);
1383 assert!(json.is_null());
1384 }
1385
1386 #[test]
1387 fn test_value_to_json_string() {
1388 let v = Value::String("hello".into());
1389 let json = value_to_json(&v);
1390 assert_eq!(json, serde_json::json!("hello"));
1391 }
1392
1393 #[test]
1394 fn test_value_to_json_bytes() {
1395 let v = Value::Bytes(vec![72, 101, 108, 108, 111]); let json = value_to_json(&v);
1397 assert!(json.is_string());
1399 assert_eq!(json.as_str().unwrap(), "SGVsbG8=");
1400 }
1401
1402 #[test]
1403 fn test_value_to_json_sequence() {
1404 let v = Value::Sequence(vec![Value::Integer(1), Value::Integer(2)]);
1405 let json = value_to_json(&v);
1406 assert!(json.is_array());
1407 assert_eq!(json, serde_json::json!([1, 2]));
1408 }
1409
1410 #[test]
1411 fn test_value_to_json_mapping() {
1412 let mut map = indexmap::IndexMap::new();
1413 map.insert("key".to_string(), Value::String("value".into()));
1414 let v = Value::Mapping(map);
1415 let json = value_to_json(&v);
1416 assert!(json.is_object());
1417 assert_eq!(json["key"], "value");
1418 }
1419
1420 #[test]
1421 fn test_format_json_value_null() {
1422 let v = serde_json::Value::Null;
1423 assert_eq!(format_json_value(&v), "null");
1424 }
1425
1426 #[test]
1427 fn test_format_json_value_bool() {
1428 assert_eq!(format_json_value(&serde_json::json!(true)), "true");
1429 assert_eq!(format_json_value(&serde_json::json!(false)), "false");
1430 }
1431
1432 #[test]
1433 fn test_format_json_value_number() {
1434 assert_eq!(format_json_value(&serde_json::json!(42)), "42");
1435 assert_eq!(format_json_value(&serde_json::json!(2.71)), "2.71");
1436 }
1437
1438 #[test]
1439 fn test_format_json_value_string_simple() {
1440 assert_eq!(format_json_value(&serde_json::json!("hello")), "hello");
1441 }
1442
1443 #[test]
1444 fn test_format_json_value_string_needs_quoting() {
1445 assert_eq!(format_json_value(&serde_json::json!("")), "\"\"");
1447 assert_eq!(
1449 format_json_value(&serde_json::json!("key:value")),
1450 "\"key:value\""
1451 );
1452 assert_eq!(
1454 format_json_value(&serde_json::json!("has#comment")),
1455 "\"has#comment\""
1456 );
1457 assert_eq!(
1459 format_json_value(&serde_json::json!(" leading")),
1460 "\" leading\""
1461 );
1462 assert_eq!(
1464 format_json_value(&serde_json::json!("trailing ")),
1465 "\"trailing \""
1466 );
1467 }
1468
1469 #[test]
1470 fn test_format_json_value_string_with_quotes_needing_escape() {
1471 let v = serde_json::Value::String("has:\"quotes\"".to_string());
1474 let formatted = format_json_value(&v);
1475 assert!(formatted.contains("\\\""));
1477 assert!(formatted.starts_with('"'));
1478 }
1479
1480 #[test]
1481 fn test_format_json_value_string_no_quoting_needed() {
1482 let v = serde_json::Value::String("has \"quotes\"".to_string());
1484 let formatted = format_json_value(&v);
1485 assert_eq!(formatted, "has \"quotes\"");
1487 }
1488
1489 #[test]
1490 fn test_format_json_value_array_empty() {
1491 assert_eq!(format_json_value(&serde_json::json!([])), "[]");
1492 }
1493
1494 #[test]
1495 fn test_format_json_value_array_with_items() {
1496 assert_eq!(
1497 format_json_value(&serde_json::json!([1, 2, 3])),
1498 "[1, 2, 3]"
1499 );
1500 }
1501
1502 #[test]
1503 fn test_format_json_value_object() {
1504 assert_eq!(format_json_value(&serde_json::json!({})), "{}");
1505 }
1506
1507 #[test]
1508 fn test_get_type_string_basic() {
1509 let schema = serde_json::json!({"type": "string"});
1510 assert_eq!(get_type_string(&schema), "string");
1511 }
1512
1513 #[test]
1514 fn test_get_type_string_with_constraints() {
1515 let schema = serde_json::json!({
1516 "type": "integer",
1517 "minimum": 1,
1518 "maximum": 100
1519 });
1520 let type_str = get_type_string(&schema);
1521 assert!(type_str.contains("integer"));
1522 assert!(type_str.contains("min: 1"));
1523 assert!(type_str.contains("max: 100"));
1524 }
1525
1526 #[test]
1527 fn test_get_type_string_with_string_constraints() {
1528 let schema = serde_json::json!({
1529 "type": "string",
1530 "minLength": 1,
1531 "maxLength": 50,
1532 "pattern": "^[a-z]+$"
1533 });
1534 let type_str = get_type_string(&schema);
1535 assert!(type_str.contains("string"));
1536 assert!(type_str.contains("minLength: 1"));
1537 assert!(type_str.contains("maxLength: 50"));
1538 assert!(type_str.contains("pattern:"));
1539 }
1540
1541 #[test]
1542 fn test_get_type_string_enum() {
1543 let schema = serde_json::json!({
1544 "enum": ["a", "b", "c"]
1545 });
1546 let type_str = get_type_string(&schema);
1547 assert!(type_str.starts_with("enum:"));
1548 assert!(type_str.contains("\"a\""));
1549 assert!(type_str.contains("\"b\""));
1550 }
1551
1552 #[test]
1553 fn test_get_type_string_enum_numeric() {
1554 let schema = serde_json::json!({
1555 "enum": [1, 2, 3]
1556 });
1557 let type_str = get_type_string(&schema);
1558 assert!(type_str.contains("1"));
1559 assert!(type_str.contains("2"));
1560 }
1561
1562 #[test]
1563 fn test_get_type_string_no_type() {
1564 let schema = serde_json::json!({});
1565 assert_eq!(get_type_string(&schema), "any");
1566 }
1567
1568 #[test]
1569 fn test_schema_default_string_with_default() {
1570 let schema = serde_json::json!({"default": 42});
1571 assert_eq!(schema_default_string(&schema), "42");
1572 }
1573
1574 #[test]
1575 fn test_schema_default_string_with_string_default() {
1576 let schema = serde_json::json!({"default": "hello"});
1577 assert_eq!(schema_default_string(&schema), "\"hello\"");
1578 }
1579
1580 #[test]
1581 fn test_schema_default_string_no_default() {
1582 let schema = serde_json::json!({});
1583 assert_eq!(schema_default_string(&schema), "-");
1584 }
1585
1586 #[test]
1587 fn test_get_template_value_with_default() {
1588 let schema = serde_json::json!({"type": "string", "default": "myvalue"});
1589 assert_eq!(get_template_value(&schema, "string"), "myvalue");
1590 }
1591
1592 #[test]
1593 fn test_get_template_value_with_enum() {
1594 let schema = serde_json::json!({"type": "string", "enum": ["first", "second"]});
1595 assert_eq!(get_template_value(&schema, "string"), "first");
1596 }
1597
1598 #[test]
1599 fn test_get_template_value_placeholders() {
1600 assert_eq!(get_template_value(&serde_json::json!({}), "string"), "\"\"");
1601 assert_eq!(get_template_value(&serde_json::json!({}), "integer"), "0");
1602 assert_eq!(get_template_value(&serde_json::json!({}), "number"), "0.0");
1603 assert_eq!(
1604 get_template_value(&serde_json::json!({}), "boolean"),
1605 "false"
1606 );
1607 assert_eq!(get_template_value(&serde_json::json!({}), "array"), "[]");
1608 assert_eq!(get_template_value(&serde_json::json!({}), "null"), "null");
1609 assert_eq!(
1610 get_template_value(&serde_json::json!({}), "unknown"),
1611 "null"
1612 );
1613 }
1614
1615 #[test]
1616 fn test_schema_to_markdown_no_title() {
1617 let schema = Schema::from_yaml(
1619 r#"
1620type: object
1621properties:
1622 name:
1623 type: string
1624"#,
1625 )
1626 .unwrap();
1627
1628 let markdown = schema.to_markdown();
1629 assert!(markdown.contains("# Configuration Reference"));
1630 }
1631
1632 #[test]
1633 fn test_schema_to_markdown_non_object_property() {
1634 let schema = Schema::from_yaml(
1636 r#"
1637type: object
1638required:
1639 - port
1640properties:
1641 port:
1642 type: integer
1643 description: Server port
1644"#,
1645 )
1646 .unwrap();
1647
1648 let markdown = schema.to_markdown();
1649 assert!(markdown.contains("port"));
1650 assert!(markdown.contains("(required)"));
1651 }
1652
1653 #[test]
1654 fn test_schema_to_template_no_title() {
1655 let schema = Schema::from_yaml(
1656 r#"
1657type: object
1658properties:
1659 name:
1660 type: string
1661"#,
1662 )
1663 .unwrap();
1664
1665 let template = schema.to_template();
1666 assert!(template.contains("Configuration template generated from schema"));
1667 }
1668
1669 #[test]
1670 fn test_schema_to_template_with_description() {
1671 let schema = Schema::from_yaml(
1672 r#"
1673type: object
1674properties:
1675 name:
1676 type: string
1677 description: The name field
1678"#,
1679 )
1680 .unwrap();
1681
1682 let template = schema.to_template();
1683 assert!(template.contains("The name field"));
1684 }
1685
1686 #[test]
1687 fn test_schema_to_template_with_default_and_description() {
1688 let schema = Schema::from_yaml(
1689 r#"
1690type: object
1691properties:
1692 port:
1693 type: integer
1694 description: Server port
1695 default: 8080
1696"#,
1697 )
1698 .unwrap();
1699
1700 let template = schema.to_template();
1701 assert!(template.contains("8080"));
1702 assert!(template.contains("default:"));
1703 }
1704
1705 #[test]
1708 fn test_get_default_simple() {
1709 let schema = Schema::from_yaml(
1710 r#"
1711type: object
1712properties:
1713 pool_size:
1714 type: integer
1715 default: 10
1716 timeout:
1717 type: number
1718 default: 30.5
1719 enabled:
1720 type: boolean
1721 default: true
1722 name:
1723 type: string
1724 default: "default_name"
1725"#,
1726 )
1727 .unwrap();
1728
1729 assert_eq!(schema.get_default("pool_size"), Some(Value::Integer(10)));
1730 assert_eq!(schema.get_default("timeout"), Some(Value::Float(30.5)));
1731 assert_eq!(schema.get_default("enabled"), Some(Value::Bool(true)));
1732 assert_eq!(
1733 schema.get_default("name"),
1734 Some(Value::String("default_name".into()))
1735 );
1736 assert_eq!(schema.get_default("nonexistent"), None);
1737 }
1738
1739 #[test]
1740 fn test_get_default_nested() {
1741 let schema = Schema::from_yaml(
1742 r#"
1743type: object
1744properties:
1745 database:
1746 type: object
1747 properties:
1748 host:
1749 type: string
1750 default: localhost
1751 port:
1752 type: integer
1753 default: 5432
1754 pool:
1755 type: object
1756 properties:
1757 size:
1758 type: integer
1759 default: 10
1760"#,
1761 )
1762 .unwrap();
1763
1764 assert_eq!(
1765 schema.get_default("database.host"),
1766 Some(Value::String("localhost".into()))
1767 );
1768 assert_eq!(
1769 schema.get_default("database.port"),
1770 Some(Value::Integer(5432))
1771 );
1772 assert_eq!(
1773 schema.get_default("database.pool.size"),
1774 Some(Value::Integer(10))
1775 );
1776 assert_eq!(schema.get_default("database.nonexistent"), None);
1777 }
1778
1779 #[test]
1780 fn test_get_default_object_level() {
1781 let schema = Schema::from_yaml(
1782 r#"
1783type: object
1784properties:
1785 logging:
1786 type: object
1787 default:
1788 level: info
1789 format: json
1790"#,
1791 )
1792 .unwrap();
1793
1794 let default = schema.get_default("logging").unwrap();
1795 match default {
1796 Value::Mapping(map) => {
1797 assert_eq!(map.get("level"), Some(&Value::String("info".into())));
1798 assert_eq!(map.get("format"), Some(&Value::String("json".into())));
1799 }
1800 _ => panic!("Expected mapping default"),
1801 }
1802 }
1803
1804 #[test]
1805 fn test_get_default_null_default() {
1806 let schema = Schema::from_yaml(
1807 r#"
1808type: object
1809properties:
1810 optional_value:
1811 type:
1812 - string
1813 - "null"
1814 default: null
1815"#,
1816 )
1817 .unwrap();
1818
1819 assert_eq!(schema.get_default("optional_value"), Some(Value::Null));
1820 }
1821
1822 #[test]
1823 fn test_allows_null_single_type() {
1824 let schema = Schema::from_yaml(
1825 r#"
1826type: object
1827properties:
1828 required_string:
1829 type: string
1830 nullable_string:
1831 type: "null"
1832"#,
1833 )
1834 .unwrap();
1835
1836 assert!(!schema.allows_null("required_string"));
1837 assert!(schema.allows_null("nullable_string"));
1838 }
1839
1840 #[test]
1841 fn test_allows_null_array_type() {
1842 let schema = Schema::from_yaml(
1843 r#"
1844type: object
1845properties:
1846 nullable_value:
1847 type:
1848 - string
1849 - "null"
1850 non_nullable:
1851 type:
1852 - string
1853 - integer
1854"#,
1855 )
1856 .unwrap();
1857
1858 assert!(schema.allows_null("nullable_value"));
1859 assert!(!schema.allows_null("non_nullable"));
1860 }
1861
1862 #[test]
1863 fn test_allows_null_nested() {
1864 let schema = Schema::from_yaml(
1865 r#"
1866type: object
1867properties:
1868 database:
1869 type: object
1870 properties:
1871 connection_string:
1872 type:
1873 - string
1874 - "null"
1875 default: null
1876"#,
1877 )
1878 .unwrap();
1879
1880 assert!(schema.allows_null("database.connection_string"));
1881 }
1882
1883 #[test]
1884 fn test_allows_null_no_type_specified() {
1885 let schema = Schema::from_yaml(
1887 r#"
1888type: object
1889properties:
1890 any_value: {}
1891"#,
1892 )
1893 .unwrap();
1894
1895 assert!(schema.allows_null("any_value"));
1896 }
1897}