1use std::path::Path;
4use std::time::Duration;
5
6use reqwest::Url;
7use reqwest::blocking::Client;
8use saphyr::LoadableYamlNode;
9use saphyr::MarkedYaml;
10use saphyr::Scalar;
11use saphyr::YamlData;
12use url::Url as ParseUrl;
13
14use crate::Error;
15use crate::Number;
16use crate::Result;
17use crate::RootSchema;
18use crate::schemas::BooleanOrSchema;
19use crate::schemas::YamlSchema;
20use crate::utils::format_marker;
21use crate::utils::scalar_to_string;
22use crate::utils::try_unwrap_saphyr_scalar;
23
24pub fn load_file<S: AsRef<str>>(path: S) -> Result<RootSchema> {
28 let fs_metadata = std::fs::metadata(path.as_ref())?;
29 if !fs_metadata.is_file() {
30 return Err(Error::FileNotFound(path.as_ref().to_string()));
31 }
32 let s = std::fs::read_to_string(path.as_ref())?;
33 let mut root = load_from_str(&s)?;
34 let canonical = Path::new(path.as_ref()).canonicalize()?;
35 root.base_uri = Some(
36 ParseUrl::from_file_path(canonical)
37 .map_err(|_| Error::GenericError("Failed to convert file path to URL".to_string()))?,
38 );
39 Ok(root)
40}
41
42pub fn load_from_str(s: &str) -> Result<RootSchema> {
44 let docs = MarkedYaml::load_from_str(s).map_err(Error::YamlParsingError)?;
45 load_from_docs(docs)
46}
47
48pub fn load_from_docs<'f>(docs: Vec<MarkedYaml<'f>>) -> Result<RootSchema> {
50 let Some(first_doc) = docs.first() else {
51 return Ok(RootSchema::empty());
52 };
53 load_from_doc(first_doc)
54}
55
56pub fn load_from_doc<'f>(doc: &MarkedYaml<'f>) -> Result<RootSchema> {
58 RootSchema::try_from(doc)
59}
60
61#[derive(thiserror::Error, Debug)]
63pub enum UrlLoadError {
64 #[error("Failed to download from URL: {0}")]
65 DownloadError(#[from] reqwest::Error),
66
67 #[error("Failed to parse URL: {0}")]
68 ParseUrlError(#[from] url::ParseError),
69
70 #[error("Failed to parse YAML: {0}")]
71 ParseError(#[from] saphyr::ScanError),
72
73 #[error("No YAML documents found in the downloaded content")]
74 NoDocuments,
75}
76
77impl From<reqwest::Error> for crate::Error {
78 fn from(value: reqwest::Error) -> Self {
79 crate::Error::UrlLoadError(UrlLoadError::DownloadError(value))
80 }
81}
82
83pub fn load_from_content(content: &str, base_uri: Option<ParseUrl>) -> Result<RootSchema> {
85 let docs = MarkedYaml::load_from_str(content).map_err(Error::YamlParsingError)?;
86 let doc = docs
87 .first()
88 .ok_or_else(|| crate::generic_error!("No YAML documents in content"))?;
89 let mut root = load_from_doc(doc)?;
90 root.base_uri = base_uri;
91 Ok(root)
92}
93
94pub fn load_external_schema(doc_url: &str) -> Result<RootSchema> {
96 let parsed = ParseUrl::parse(doc_url).map_err(|e| Error::UrlLoadError(e.into()))?;
97 match parsed.scheme() {
98 "file" => {
99 let path = parsed
100 .to_file_path()
101 .map_err(|_| Error::GenericError("Invalid file URL".to_string()))?;
102 let path_str = path
103 .to_str()
104 .ok_or_else(|| Error::GenericError("Non-UTF-8 file path".to_string()))?;
105 load_file(path_str)
106 }
107 "http" | "https" => {
108 let (content, url) = fetch_url(doc_url, None)?;
109 load_from_content(&content, Some(url))
110 }
111 _ => Err(Error::GenericError(format!(
112 "Unsupported URL scheme for $ref: {}",
113 parsed.scheme()
114 ))),
115 }
116}
117
118pub fn extract_dollar_schema_from_yaml(contents: &str) -> Result<Option<String>> {
123 let docs = MarkedYaml::load_from_str(contents).map_err(Error::YamlParsingError)?;
124 let Some(first) = docs.first() else {
125 return Ok(None);
126 };
127 match &first.data {
128 YamlData::Mapping(mapping) => {
129 let key = MarkedYaml::value_from_str("$schema");
130 match mapping.get(&key) {
131 Some(v) => Ok(Some(marked_yaml_to_string(v, "$schema must be a string")?)),
132 None => Ok(None),
133 }
134 }
135 _ => Ok(None),
136 }
137}
138
139pub fn load_root_schema_from_ref(
145 schema_ref: &str,
146 instance_parent: &Path,
147) -> Result<(RootSchema, String)> {
148 let trimmed = schema_ref.trim();
149 if trimmed.is_empty() {
150 return Err(crate::generic_error!("$schema value is empty"));
151 }
152
153 let root = match ParseUrl::parse(trimmed) {
154 Ok(parsed) if matches!(parsed.scheme(), "http" | "https" | "file") => {
155 load_external_schema(trimmed)?
156 }
157 Ok(parsed) => {
158 return Err(crate::generic_error!(
159 "Unsupported URL scheme in $schema: {}",
160 parsed.scheme()
161 ));
162 }
163 Err(_) => {
164 let path = Path::new(trimmed);
165 let resolved = if path.is_absolute() {
166 path.to_path_buf()
167 } else {
168 instance_parent.join(path)
169 };
170 let path_str = resolved
171 .to_str()
172 .ok_or_else(|| Error::GenericError("Non-UTF-8 schema path".to_string()))?;
173 load_file(path_str)?
174 }
175 };
176
177 let fallback = root
178 .base_uri
179 .as_ref()
180 .map(|u| u.to_string())
181 .ok_or_else(|| {
182 Error::GenericError("Internal error: loaded schema missing base URI".to_string())
183 })?;
184
185 Ok((root, fallback))
186}
187
188pub fn fetch_url(url_string: &str, timeout_seconds: Option<u64>) -> Result<(String, Url)> {
193 let url_owned = url_string.to_string();
194 let timeout = Duration::from_secs(timeout_seconds.unwrap_or(30));
195
196 std::thread::spawn(move || {
197 let client = Client::builder()
199 .timeout(timeout)
200 .use_rustls_tls()
201 .build()?;
202
203 let url = Url::parse(&url_owned).map_err(|e| Error::UrlLoadError(e.into()))?;
204
205 let response = client.get(url.clone()).send()?;
206 if !response.status().is_success() {
207 match response.error_for_status() {
208 Ok(_) => unreachable!(),
209 Err(e) => return Err(e.into()),
210 }
211 }
212
213 let content = response.text()?;
214 Ok((content, url))
215 })
216 .join()
217 .unwrap_or_else(|_| {
218 Err(Error::GenericError(
219 "HTTP fetch thread panicked".to_string(),
220 ))
221 })
222}
223
224pub fn download_from_url(url_string: &str, timeout_seconds: Option<u64>) -> Result<RootSchema> {
240 let (yaml_content, url) = fetch_url(url_string, timeout_seconds)?;
241
242 let docs = MarkedYaml::load_from_str(&yaml_content).map_err(UrlLoadError::ParseError)?;
244
245 match docs.first() {
246 Some(doc) => {
247 let mut root = load_from_doc(doc)?;
248 root.base_uri = Some(url);
249 Ok(root)
250 }
251 None => Err(UrlLoadError::NoDocuments.into()),
252 }
253}
254
255pub fn marked_yaml_to_string<S: Into<String> + Copy>(yaml: &MarkedYaml, msg: S) -> Result<String> {
256 if let YamlData::Value(Scalar::String(s)) = &yaml.data {
257 Ok(s.to_string())
258 } else {
259 Err(Error::ExpectedScalar(msg.into()))
260 }
261}
262
263pub fn marked_yaml_mapping_key_to_string(yaml: &MarkedYaml) -> Result<String> {
268 if let YamlData::Value(scalar) = &yaml.data {
269 Ok(scalar_to_string(scalar))
270 } else {
271 Err(expected_scalar!(
272 "[{}] Expected a scalar mapping key, got: {:?}",
273 format_marker(&yaml.span.start),
274 yaml
275 ))
276 }
277}
278
279pub fn load_array_of_schemas_marked<'f>(value: &MarkedYaml<'f>) -> Result<Vec<YamlSchema>> {
280 if let YamlData::Sequence(values) = &value.data {
281 values
282 .iter()
283 .map(|v| {
284 if v.is_mapping() {
285 v.try_into()
286 } else {
287 Err(generic_error!("Expected a mapping, but got: {:?}", v))
288 }
289 })
290 .collect::<Result<Vec<YamlSchema>>>()
291 } else {
292 Err(generic_error!(
293 "{} Expected a sequence, but got: {:?}",
294 format_marker(&value.span.start),
295 value
296 ))
297 }
298}
299
300pub fn load_integer(value: &saphyr::Yaml) -> Result<i64> {
301 let scalar = try_unwrap_saphyr_scalar(value)?;
302 match scalar {
303 saphyr::Scalar::Integer(i) => Ok(*i),
304 _ => Err(unsupported_type!(
305 "Expected type: integer, but got: {:?}",
306 value
307 )),
308 }
309}
310
311pub fn load_integer_marked(value: &MarkedYaml) -> Result<i64> {
312 if let YamlData::Value(Scalar::Integer(i)) = &value.data {
313 Ok(*i)
314 } else {
315 Err(generic_error!(
316 "{} Expected integer value, got: {:?}",
317 format_marker(&value.span.start),
318 value
319 ))
320 }
321}
322
323pub fn load_number(value: &saphyr::Yaml) -> Result<Number> {
324 let scalar = try_unwrap_saphyr_scalar(value)?;
325 match scalar {
326 Scalar::Integer(i) => Ok(Number::integer(*i)),
327 Scalar::FloatingPoint(o) => Ok(Number::float(o.into_inner())),
328 _ => Err(unsupported_type!(
329 "Expected type: integer or float, but got: {:?}",
330 value
331 )),
332 }
333}
334
335pub fn load_array_items_marked<'input>(value: &MarkedYaml<'input>) -> Result<BooleanOrSchema> {
336 match &value.data {
337 YamlData::Value(scalar) => {
338 if let Scalar::Boolean(b) = scalar {
339 Ok(BooleanOrSchema::Boolean(*b))
340 } else {
341 Err(generic_error!(
342 "array: boolean or mapping with type or $ref, but got: {:?}",
343 value
344 ))
345 }
346 }
347 YamlData::Mapping(_mapping) => {
348 let schema: YamlSchema = value.try_into()?;
349 Ok(BooleanOrSchema::schema(schema))
350 }
351 _ => Err(generic_error!(
352 "array: boolean or mapping with type or $ref, but got: {:?}",
353 value
354 )),
355 }
356}
357
358pub fn load_boolean_or_schema_marked(value: &MarkedYaml<'_>) -> Result<BooleanOrSchema> {
360 match &value.data {
361 YamlData::Value(scalar) => match scalar {
362 Scalar::Boolean(b) => Ok(BooleanOrSchema::Boolean(*b)),
363 _ => Err(generic_error!(
364 "{} Expected a boolean scalar, but got: {:?}",
365 format_marker(&value.span.start),
366 scalar
367 )),
368 },
369 YamlData::Mapping(_) => {
370 let schema: YamlSchema = value.try_into()?;
371 Ok(BooleanOrSchema::schema(schema))
372 }
373 _ => Err(unsupported_type!(
374 "Expected boolean or mapping, but got: {:?}",
375 value
376 )),
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use regex::Regex;
383 use saphyr::LoadableYamlNode;
384 use saphyr::MarkedYaml;
385
386 use crate::ConstValue;
387 use crate::Engine;
388 use crate::Result;
389 use crate::Validator as _;
390 use crate::loader;
391 use crate::schemas::EnumSchema;
392 use crate::schemas::IntegerSchema;
393 use crate::schemas::SchemaType;
394 use crate::schemas::StringSchema;
395
396 use super::*;
397
398 #[test]
399 fn test_boolean_literal_true() {
400 let root_schema = load_from_doc(&MarkedYaml::value_from_str("true")).unwrap();
401 assert_eq!(root_schema.schema, YamlSchema::BooleanLiteral(true));
402 }
403
404 #[test]
405 fn test_boolean_literal_false() {
406 let root_schema = load_from_doc(&MarkedYaml::value_from_str("false")).unwrap();
407 assert_eq!(root_schema.schema, YamlSchema::BooleanLiteral(false));
408 }
409
410 #[test]
411 fn test_const_string() {
412 let docs = MarkedYaml::load_from_str("const: string value").unwrap();
413 let root_schema = load_from_doc(docs.first().unwrap()).unwrap();
414 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
415 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
416 };
417 assert_eq!(subschema.r#const, Some(ConstValue::string("string value")));
418 }
419
420 #[test]
421 fn test_const_integer() {
422 let docs = MarkedYaml::load_from_str("const: 42").unwrap();
423 let root_schema = load_from_doc(docs.first().unwrap()).unwrap();
424 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
425 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
426 };
427 assert_eq!(subschema.r#const, Some(ConstValue::integer(42)));
428 }
429
430 #[test]
431 fn test_const_array() {
432 let docs = MarkedYaml::load_from_str("const: [1, 2]").unwrap();
433 let root_schema = load_from_doc(docs.first().unwrap()).unwrap();
434 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
435 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
436 };
437 let expected = ConstValue::Array(vec![ConstValue::integer(1), ConstValue::integer(2)]);
438 assert_eq!(subschema.r#const, Some(expected));
439 }
440
441 #[test]
442 fn test_const_object() {
443 let docs = MarkedYaml::load_from_str("const:\n a: 1").unwrap();
444 let root_schema = load_from_doc(docs.first().unwrap()).unwrap();
445 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
446 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
447 };
448 let mut expected_obj = hashlink::LinkedHashMap::new();
449 expected_obj.insert("a".into(), ConstValue::integer(1));
450 assert_eq!(subschema.r#const, Some(ConstValue::Object(expected_obj)));
451 }
452
453 #[test]
454 fn test_type_foo_should_error() {
455 let docs = MarkedYaml::load_from_str("type: foo").unwrap();
456 let root_schema = load_from_doc(docs.first().unwrap());
457 assert!(root_schema.is_err());
458 assert_eq!(
459 root_schema.unwrap_err().to_string(),
460 "Unsupported type: Expected type: string, number, integer, object, array, boolean, or null, but got: foo"
461 );
462 }
463
464 #[test]
465 fn test_type_string() {
466 let docs = MarkedYaml::load_from_str("type: string").unwrap();
467 let root_schema = load_from_doc(docs.first().unwrap()).unwrap();
468 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
469 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
470 };
471 assert_eq!(subschema.r#type, SchemaType::new("string"));
472 }
473
474 #[test]
475 fn test_type_object_with_string_with_description() {
476 let root_schema = loader::load_from_str(
477 r#"
478 type: object
479 properties:
480 name:
481 type: string
482 description: This is a description
483 "#,
484 )
485 .expect("Failed to load schema");
486 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
487 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
488 };
489 let Some(object_schema) = &subschema.object_schema else {
490 panic!(
491 "Expected ObjectSchema, but got: {:?}",
492 &subschema.object_schema
493 );
494 };
495 let name_property = object_schema
496 .properties
497 .as_ref()
498 .expect("Expected properties")
499 .get("name")
500 .expect("Expected `name` property");
501
502 let YamlSchema::Subschema(name_property_schema) = &name_property else {
503 panic!(
504 "Expected Subschema for `name` property, but got: {:?}",
505 &name_property
506 );
507 };
508 assert_eq!(name_property_schema.r#type, SchemaType::new("string"));
509 assert_eq!(
510 name_property_schema.string_schema,
511 Some(StringSchema::default())
512 );
513 assert_eq!(
514 name_property_schema.metadata_and_annotations.description,
515 Some("This is a description".to_string())
516 );
517 }
518
519 #[test]
520 fn test_type_string_with_pattern() {
521 let root_schema = loader::load_from_str(
522 r#"
523 type: string
524 pattern: "^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$"
525 "#,
526 )
527 .unwrap();
528 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
529 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
530 };
531 assert_eq!(subschema.r#type, SchemaType::new("string"));
532 let expected = StringSchema {
533 pattern: Some(Regex::new("^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$").unwrap()),
534 ..Default::default()
535 };
536
537 assert_eq!(subschema.string_schema, Some(expected));
538 }
539
540 #[test]
541 fn test_integer_schema() {
542 let root_schema = loader::load_from_str("type: integer").unwrap();
543 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
544 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
545 };
546 let integer_schema = IntegerSchema::default();
547 assert_eq!(subschema.integer_schema, Some(integer_schema));
548 }
549
550 #[test]
551 fn test_enum() {
552 let root_schema = loader::load_from_str(
553 r#"
554 enum:
555 - foo
556 - bar
557 - baz
558 "#,
559 )
560 .unwrap();
561 let enum_values = ["foo", "bar", "baz"]
562 .iter()
563 .map(|s| ConstValue::string(s.to_string()))
564 .collect::<Vec<ConstValue>>();
565 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
566 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
567 };
568 assert_eq!(
569 subschema.r#enum,
570 Some(EnumSchema {
571 r#enum: enum_values
572 })
573 );
574 }
575
576 #[test]
577 fn test_enum_without_type() {
578 let root_schema = loader::load_from_str(
579 r#"
580 enum:
581 - red
582 - amber
583 - green
584 - null
585 - 42
586 "#,
587 )
588 .unwrap();
589 let enum_values = vec![
590 ConstValue::string("red".to_string()),
591 ConstValue::string("amber".to_string()),
592 ConstValue::string("green".to_string()),
593 ConstValue::null(),
594 ConstValue::integer(42),
595 ];
596 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
597 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
598 };
599 assert_eq!(
600 subschema.r#enum,
601 Some(EnumSchema {
602 r#enum: enum_values
603 })
604 );
605 }
606
607 #[test]
608 fn test_defs() {
609 let root_schema = loader::load_from_str(
610 r##"
611 $defs:
612 foo:
613 type: boolean
614 "##,
615 )
616 .unwrap();
617 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
618 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
619 };
620 assert!(subschema.defs.is_some());
621 let Some(defs) = &subschema.defs else {
622 panic!("Expected defs, but got: {:?}", &subschema.defs);
623 };
624 assert_eq!(defs.len(), 1);
625 assert_eq!(defs.get("foo"), Some(&YamlSchema::typed_boolean()));
626 }
627
628 #[test]
629 fn test_one_of_with_ref() {
630 let root_schema = loader::load_from_str(
631 r##"
632 $defs:
633 foo:
634 type: boolean
635 oneOf:
636 - type: string
637 - $ref: "#/$defs/foo"
638 "##,
639 )
640 .unwrap();
641 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
642 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
643 };
644 assert!(subschema.one_of.is_some());
645 let Some(one_of) = &subschema.one_of else {
646 panic!("Expected oneOf, but got: {:?}", &subschema.one_of);
647 };
648 assert_eq!(one_of.one_of.len(), 2);
649 assert_eq!(
650 one_of.one_of[0],
651 YamlSchema::typed_string(StringSchema::default()),
652 "one_of[0] should be a string schema"
653 );
654 assert_eq!(
655 one_of.one_of[1],
656 YamlSchema::ref_str("#/$defs/foo"),
657 "one_of[1] should be a reference to '#/$defs/foo'"
658 );
659
660 let s = r#"
661 false
662 "#;
663 let docs = MarkedYaml::load_from_str(s).unwrap();
664 let value = docs.first().unwrap();
665 let context = crate::Context::with_root_schema(&root_schema, true);
666 let result = root_schema.validate(&context, value);
667 assert!(result.is_ok());
668 assert!(!context.has_errors());
669 }
670
671 #[test]
672 fn extract_dollar_schema_from_mapping() {
673 let yaml = "$schema: ./x.yaml\nfoo: 1\n";
674 assert_eq!(
675 extract_dollar_schema_from_yaml(yaml).unwrap(),
676 Some("./x.yaml".to_string())
677 );
678 }
679
680 #[test]
681 fn extract_dollar_schema_missing() {
682 assert_eq!(extract_dollar_schema_from_yaml("foo: 1\n").unwrap(), None);
683 }
684
685 #[test]
686 fn extract_dollar_schema_non_mapping_root() {
687 assert_eq!(extract_dollar_schema_from_yaml("- a\n").unwrap(), None);
688 }
689
690 #[test]
691 fn extract_dollar_schema_not_string_errors() {
692 let result = extract_dollar_schema_from_yaml("$schema: 42\n");
693 assert!(result.is_err());
694 }
695
696 #[test]
697 fn load_root_schema_from_ref_relative_path() {
698 let dir = std::env::temp_dir().join(format!("yaml_schema_ref_test_{}", std::process::id()));
699 std::fs::create_dir_all(&dir).expect("create temp dir");
700 let schema_path = dir.join("sch.yaml");
701 std::fs::write(
702 &schema_path,
703 "type: object\nproperties:\n a:\n type: string\n",
704 )
705 .expect("write schema");
706 let (root, uri) = load_root_schema_from_ref("sch.yaml", &dir).expect("load");
707 assert!(uri.starts_with("file://"));
708 let YamlSchema::Subschema(sub) = &root.schema else {
709 panic!("expected Subschema");
710 };
711 assert_eq!(sub.r#type, SchemaType::new("object"));
712 std::fs::remove_dir_all(&dir).ok();
713 }
714
715 #[test]
716 fn test_self_validate() -> Result<()> {
717 let schema_filename = "yaml-schema.yaml";
718 let root_schema = match loader::load_file(schema_filename) {
719 Ok(schema) => schema,
720 Err(e) => {
721 eprintln!("Failed to read YAML schema file: {schema_filename}");
722 log::error!("{e}");
723 return Err(e);
724 }
725 };
726
727 let yaml_contents = std::fs::read_to_string(schema_filename)?;
728
729 let context = Engine::evaluate(&root_schema, &yaml_contents, false)?;
730 if context.has_errors() {
731 for error in context.errors.borrow().iter() {
732 eprintln!("{error}");
733 }
734 }
735 assert!(!context.has_errors());
736
737 Ok(())
738 }
739
740 #[test]
743 fn test_meta_schema_accepts_required() -> Result<()> {
744 let root_schema = loader::load_file("yaml-schema.yaml")?;
745 let instance = r#"
746type: object
747properties:
748 any_property:
749 type: string
750required:
751 - any_property
752"#;
753 let context = Engine::evaluate(&root_schema, instance, false)?;
754 if context.has_errors() {
755 for error in context.errors.borrow().iter() {
756 eprintln!("{error}");
757 }
758 }
759 assert!(!context.has_errors());
760
761 Ok(())
762 }
763
764 #[test]
765 fn test_download_from_url() {
766 if std::env::var("CI").is_ok() {
768 return;
770 }
771
772 let result = std::panic::catch_unwind(|| {
773 let url = "https://yaml-schema.net/yaml-schema.yaml";
774 let result = download_from_url(url, Some(10));
775
776 let root_schema = result.expect("Failed to download and parse YAML schema from URL");
778
779 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
781 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
782 };
783 assert_eq!(subschema.r#type, SchemaType::new("object"));
784 assert!(subschema.object_schema.is_some());
785
786 if let Ok(local_schema) = std::fs::read_to_string("yaml-schema.yaml") {
788 let context = Engine::evaluate(&root_schema, &local_schema, false);
789 if let Ok(ctx) = context {
790 if ctx.has_errors() {
791 for error in ctx.errors.borrow().iter() {
792 eprintln!("Validation error: {}", error);
793 }
794 panic!("Downloaded schema failed validation against local schema");
795 }
796 } else if let Err(e) = context {
797 panic!("Failed to validate downloaded schema: {}", e);
798 }
799 }
800 });
801
802 if let Err(e) = result {
803 if let Some(s) = e.downcast_ref::<String>()
805 && (s.contains("Network is unreachable")
806 || s.contains("failed to lookup address information"))
807 {
808 eprintln!("Warning: Network unreachable, skipping download test");
809 return;
810 }
811
812 std::panic::resume_unwind(e);
814 }
815 }
816}