1use std::collections::HashSet;
2use std::fmt::Display;
3use std::rc::Rc;
4
5use hashlink::LinkedHashMap;
6use jsonptr::Token;
7use log::debug;
8use log::error;
9use saphyr::{AnnotatedMapping, MarkedYaml, Scalar, YamlData};
10
11use crate::ConstValue;
12use crate::Context;
13use crate::Error;
14use crate::RefUri;
15use crate::Reference;
16use crate::Result;
17use crate::Validator;
18use crate::loader::load_boolean_or_schema_marked;
19use crate::loader::load_external_schema;
20use crate::loader::marked_yaml_mapping_key_to_string;
21use crate::loader::marked_yaml_to_string;
22use crate::schemas::AllOfSchema;
23use crate::schemas::AnyOfSchema;
24use crate::schemas::ArraySchema;
25use crate::schemas::EnumSchema;
26use crate::schemas::IfThenElseSchema;
27use crate::schemas::IntegerSchema;
28use crate::schemas::NotSchema;
29use crate::schemas::NumberSchema;
30use crate::schemas::ObjectSchema;
31use crate::schemas::OneOfSchema;
32use crate::schemas::StringSchema;
33use crate::utils::format_annotated_mapping;
34use crate::utils::format_linked_hash_map;
35use crate::utils::format_marked_yaml;
36use crate::utils::format_marker;
37use crate::utils::format_scalar;
38use crate::utils::format_vec;
39use crate::utils::format_yaml_data;
40use crate::utils::scalar_to_string;
41use crate::validation::ArrayUnevaluatedAnnotations;
42
43#[derive(Debug, PartialEq)]
45pub enum YamlSchema {
46 Empty, Null, BooleanLiteral(bool), Subschema(Box<Subschema>),
50}
51
52impl YamlSchema {
53 pub fn subschema(subschema: Subschema) -> Self {
54 Self::Subschema(Box::new(subschema))
55 }
56
57 pub fn ref_str(ref_name: impl Into<String>) -> Self {
58 Self::subschema(Subschema {
59 r#ref: Some(Reference::new(ref_name)),
60 ..Default::default()
61 })
62 }
63
64 pub fn typed_boolean() -> Self {
66 Self::subschema(Subschema {
67 r#type: SchemaType::new("boolean"),
68 ..Default::default()
69 })
70 }
71
72 pub fn typed_number(number_schema: NumberSchema) -> Self {
74 number_schema.into()
75 }
76
77 pub fn typed_string(string_schema: StringSchema) -> Self {
79 Self::subschema(Subschema {
80 r#type: SchemaType::new("string"),
81 string_schema: Some(string_schema),
82 ..Default::default()
83 })
84 }
85
86 pub fn typed_object(object_schema: ObjectSchema) -> Self {
88 Self::subschema(Subschema {
89 r#type: SchemaType::new("object"),
90 object_schema: Some(object_schema),
91 ..Default::default()
92 })
93 }
94
95 pub fn resolve(
97 &self,
98 key: Option<&Token>,
99 components: &[jsonptr::Component],
100 ) -> Option<&YamlSchema> {
101 debug!("[YamlSchema#resolve] self: {self}, key: {key:?}, components: {components:?}");
102 if components.is_empty() {
103 return Some(self);
104 }
105 match self {
106 YamlSchema::Subschema(subschema) => subschema.resolve(key, components),
107 _ => None,
108 }
109 }
110}
111
112impl<'r> TryFrom<&MarkedYaml<'r>> for YamlSchema {
113 type Error = crate::Error;
114 fn try_from(marked_yaml: &MarkedYaml<'r>) -> crate::Result<Self> {
115 match &marked_yaml.data {
116 YamlData::Value(scalar) => match scalar {
117 Scalar::Boolean(value) => Ok(YamlSchema::BooleanLiteral(*value)),
118 Scalar::Null => Ok(YamlSchema::Null),
119 _ => Err(generic_error!(
120 "[YamlSchema#try_from] Expected a boolean or null, but got: {}",
121 format_scalar(scalar)
122 )),
123 },
124 YamlData::Mapping(_) => Subschema::try_from(marked_yaml).map(YamlSchema::subschema),
125 _ => Err(generic_error!(
126 "[YamlSchema#try_from] Expected a boolean, null, or a mapping, but got: {}",
127 format_marked_yaml(marked_yaml)
128 )),
129 }
130 }
131}
132
133impl From<NumberSchema> for YamlSchema {
134 fn from(number_schema: NumberSchema) -> Self {
135 YamlSchema::subschema(Subschema {
136 r#type: SchemaType::new("number"),
137 number_schema: Some(number_schema),
138 ..Default::default()
139 })
140 }
141}
142
143impl From<IntegerSchema> for YamlSchema {
144 fn from(integer_schema: IntegerSchema) -> Self {
145 YamlSchema::subschema(Subschema {
146 r#type: SchemaType::new("integer"),
147 integer_schema: Some(integer_schema),
148 ..Default::default()
149 })
150 }
151}
152
153impl From<StringSchema> for YamlSchema {
154 fn from(string_schema: StringSchema) -> Self {
155 YamlSchema::subschema(Subschema {
156 r#type: SchemaType::new("string"),
157 string_schema: Some(string_schema),
158 ..Default::default()
159 })
160 }
161}
162
163impl Validator for YamlSchema {
164 fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
165 debug!("[YamlSchema] self: {self}");
166 debug!(
167 "[YamlSchema] Validating value: {}",
168 format_yaml_data(&value.data)
169 );
170 match self {
171 YamlSchema::Empty => Ok(()),
172 YamlSchema::Null => {
173 if !matches!(&value.data, YamlData::Value(Scalar::Null)) {
174 context.add_error(
175 value,
176 format!("Expected null, but got: {}", format_yaml_data(&value.data)),
177 );
178 }
179 Ok(())
180 }
181 YamlSchema::BooleanLiteral(boolean) => {
182 if !*boolean {
183 context.add_error(value, "YamlSchema is `false`!");
184 }
185 Ok(())
186 }
187 YamlSchema::Subschema(subschema) => {
188 debug!("[YamlSchema#validate] Validating subschema: {subschema:?}");
189 subschema.validate(context, value)?;
190 Ok(())
191 }
192 }
193 }
194}
195
196impl From<Subschema> for YamlSchema {
197 fn from(subschema: Subschema) -> Self {
198 YamlSchema::subschema(subschema)
199 }
200}
201
202impl Display for YamlSchema {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 match self {
205 YamlSchema::Empty => write!(f, "<empty>"),
206 YamlSchema::Null => write!(f, "null"),
207 YamlSchema::BooleanLiteral(value) => write!(f, "{value}"),
208 YamlSchema::Subschema(subschema) => subschema.fmt(f),
209 }
210 }
211}
212
213#[derive(Debug, PartialEq)]
215pub enum BooleanOrSchema {
216 Boolean(bool),
217 Schema(YamlSchema),
218}
219
220impl BooleanOrSchema {
221 pub fn schema(schema: YamlSchema) -> Self {
222 BooleanOrSchema::Schema(schema)
223 }
224}
225
226impl Display for BooleanOrSchema {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 match self {
229 BooleanOrSchema::Boolean(value) => write!(f, "{value}"),
230 BooleanOrSchema::Schema(schema) => schema.fmt(f),
231 }
232 }
233}
234
235#[derive(Debug, Default, PartialEq)]
236pub enum SchemaType {
237 #[default]
238 None,
240 Single(String),
242 Multiple(Vec<String>),
244}
245
246impl SchemaType {
247 pub fn new<S: Into<String>>(value: S) -> Self {
249 SchemaType::Single(value.into())
250 }
251
252 pub fn is_none(&self) -> bool {
253 matches!(self, SchemaType::None)
254 }
255
256 pub fn is_single(&self) -> bool {
257 matches!(self, SchemaType::Single(_))
258 }
259
260 pub fn is_multiple(&self) -> bool {
261 matches!(self, SchemaType::Multiple(_))
262 }
263
264 pub fn is_or_contains(&self, r#type: &str) -> bool {
295 match self {
296 SchemaType::None => false,
297 SchemaType::Single(s) => s == r#type,
298 SchemaType::Multiple(values) => values.contains(&r#type.to_string()),
299 }
300 }
301
302 pub fn is_none_or_string(&self) -> bool {
305 match self {
306 SchemaType::None => true,
307 SchemaType::Single(s) => s == "string",
308 SchemaType::Multiple(_) => false,
309 }
310 }
311}
312
313impl Display for SchemaType {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 match self {
316 SchemaType::None => Ok(()), SchemaType::Single(value) => write!(f, "{value}"),
318 SchemaType::Multiple(values) => write!(f, "{}", format_vec(values)),
319 }
320 }
321}
322
323#[derive(Debug, Default, PartialEq)]
325pub struct Subschema {
326 pub metadata_and_annotations: MetadataAndAnnotations,
328 pub anchor: Option<String>,
330 pub r#ref: Option<Reference>,
332 pub defs: Option<LinkedHashMap<String, YamlSchema>>,
334 pub any_of: Option<AnyOfSchema>,
336 pub all_of: Option<AllOfSchema>,
338 pub one_of: Option<OneOfSchema>,
340 pub not: Option<NotSchema>,
342 pub if_then_else: Option<IfThenElseSchema>,
344 pub r#type: SchemaType,
346 pub r#const: Option<ConstValue>,
348 pub r#enum: Option<EnumSchema>,
350
351 pub array_schema: Option<ArraySchema>,
352 pub integer_schema: Option<IntegerSchema>,
353 pub number_schema: Option<NumberSchema>,
354 pub object_schema: Option<ObjectSchema>,
355 pub string_schema: Option<StringSchema>,
356 pub unevaluated_properties: Option<BooleanOrSchema>,
358 pub unevaluated_items: Option<BooleanOrSchema>,
360}
361
362impl Subschema {
363 pub fn resolve(
365 &self,
366 token: Option<&Token>,
367 components: &[jsonptr::Component],
368 ) -> Option<&YamlSchema> {
369 debug!("[Subschema#resolve] self: {self}, token: {token:?}, components: {components:?}");
370 if let Some(token) = token {
371 let s = token.decoded();
372 debug!("[Subschema#resolve] key: {s}");
373 match s.as_ref() {
374 "$defs" => {
375 debug!("[Subschema#resolve] Resolving $defs");
376 if let Some(defs) = self.defs.as_ref() {
377 debug!("[Subschema#resolve] defs: {}", format_linked_hash_map(defs));
378 if let Some(component) = components.first() {
379 debug!("[Subschema#resolve] component: {component:?}");
380 if let jsonptr::Component::Token(next_token) = component {
381 let decoded = next_token.decoded();
382 debug!("[Subschema#resolve] decoded: {decoded}");
383 debug!("[Subschema#resolve] defs: {defs:?}");
384 if let Some(schema) = defs.get(decoded.as_ref()) {
385 debug!("[Subschema#resolve] schema: {schema:?}");
386 return schema.resolve(Some(next_token), &components[1..]);
387 }
388 }
389 }
390 }
391 }
392 "anyOf" => {}
393 _ => (),
394 }
395 }
396 None
397 }
398}
399
400impl<'r> TryFrom<&MarkedYaml<'r>> for Subschema {
403 type Error = crate::Error;
404 fn try_from(marked_yaml: &MarkedYaml<'r>) -> crate::Result<Self> {
405 if let YamlData::Mapping(mapping) = &marked_yaml.data {
406 Self::try_from(mapping)
407 } else {
408 Err(generic_error!(
409 "{} Expected a mapping, but got: {:?}",
410 format_marker(&marked_yaml.span.start),
411 marked_yaml
412 ))
413 }
414 }
415}
416
417fn try_load_defs<'r>(marked_yaml: &MarkedYaml<'r>) -> Result<LinkedHashMap<String, YamlSchema>> {
418 debug!(
419 "[try_load_defs] marked_yaml: {}",
420 format_yaml_data(&marked_yaml.data)
421 );
422 if let YamlData::Mapping(mapping) = &marked_yaml.data {
423 debug!(
424 "[try_load_defs] mapping: {}",
425 format_annotated_mapping(mapping)
426 );
427 mapping
428 .iter()
429 .try_fold(LinkedHashMap::new(), |mut acc, (key, value)| {
430 let key = marked_yaml_mapping_key_to_string(key)?;
431 acc.insert(key, value.try_into()?);
432 Ok(acc)
433 })
434 } else {
435 Err(expected_mapping!(marked_yaml))
436 }
437}
438
439impl<'r> TryFrom<&AnnotatedMapping<'r, MarkedYaml<'r>>> for Subschema {
440 type Error = Error;
441
442 fn try_from(mapping: &AnnotatedMapping<'r, MarkedYaml<'r>>) -> crate::Result<Self> {
443 debug!(
444 "[Subschema#try_from] mapping has {} keys",
445 mapping.keys().len()
446 );
447 for key in mapping.keys() {
448 debug!("[Subschema#try_from] key: {:?}", key.data);
449 }
450
451 let metadata_and_annotations = MetadataAndAnnotations::try_from(mapping)?;
452 debug!("[Subschema#try_from] metadata_and_annotations: {metadata_and_annotations}");
453
454 let defs: Option<LinkedHashMap<String, YamlSchema>> = mapping
456 .get(&MarkedYaml::value_from_str("$defs"))
457 .map(|x| {
458 debug!("[Subschema#try_from] x: {}", format_yaml_data(&x.data));
459 debug!("[Subschema#try_from] Trying to load `$defs` as LinkedHashMap<String, YamlSchema>");
460 try_load_defs(x)
461 })
462 .transpose()?;
463
464 let reference: Option<Reference> = mapping
466 .get(&MarkedYaml::value_from_str("$ref"))
467 .map(|_| {
468 debug!("[Subschema#try_from] Trying to load `$ref` as Reference");
469 mapping.try_into()
470 })
471 .transpose()?;
472
473 let any_of: Option<AnyOfSchema> = mapping
475 .get(&MarkedYaml::value_from_str("anyOf"))
476 .map(|_| {
477 debug!("[Subschema#try_from] Trying to load `anyOf` as AnyOfSchema");
478 mapping.try_into()
479 })
480 .transpose()?;
481
482 let all_of: Option<AllOfSchema> = mapping
484 .get(&MarkedYaml::value_from_str("allOf"))
485 .map(|_| {
486 debug!("[Subschema#try_from] Trying to load `allOf` as AllOfSchema");
487 mapping.try_into()
488 })
489 .transpose()?;
490
491 let one_of: Option<OneOfSchema> = mapping
493 .get(&MarkedYaml::value_from_str("oneOf"))
494 .map(|_| {
495 debug!("[Subschema#try_from] Trying to load `oneOf` as OneOfSchema");
496 mapping.try_into()
497 })
498 .transpose()?;
499
500 let not: Option<NotSchema> = mapping
502 .get(&MarkedYaml::value_from_str("not"))
503 .map(|_| {
504 debug!("[Subschema#try_from] Trying to load `not` as NotSchema");
505 mapping.try_into()
506 })
507 .transpose()?;
508
509 let if_then_else: Option<IfThenElseSchema> = mapping
511 .get(&MarkedYaml::value_from_str("if"))
512 .map(|_| {
513 debug!(
514 "[Subschema#try_from] Trying to load `if`/`then`/`else` as IfThenElseSchema"
515 );
516 IfThenElseSchema::try_from(mapping)
517 })
518 .transpose()?;
519
520 let mut r#const: Option<ConstValue> = None;
522 if let Some(value) = mapping.get(&MarkedYaml::value_from_str("const")) {
523 r#const = Some(ConstValue::try_from(value)?);
524 }
525
526 let mut r#enum: Option<EnumSchema> = None;
528 if let Some(value) = mapping.get(&MarkedYaml::value_from_str("enum")) {
529 r#enum = Some(value.try_into()?);
530 }
531
532 let mut r#type: SchemaType = SchemaType::None;
534 if let Some(type_value) = mapping.get(&MarkedYaml::value_from_str("type")) {
535 match &type_value.data {
536 YamlData::Value(Scalar::Null) => {
537 r#type = SchemaType::new("null");
538 }
539 YamlData::Value(Scalar::String(s)) => r#type = SchemaType::new(s.as_ref()),
540 YamlData::Sequence(values) => {
541 r#type = SchemaType::Multiple(
542 values
543 .iter()
544 .map(|marked_yaml| {
545 marked_yaml_to_string(marked_yaml, "type must be a string")
546 })
547 .collect::<Result<Vec<String>>>()?,
548 )
549 }
550 _ => {
551 return Err(schema_loading_error!(
552 "[Subschema#try_from] Expected a string or sequence for `type`, but got: {:?}",
553 type_value.data
554 ));
555 }
556 }
557 }
558
559 let mut array_schema = None;
561 let mut integer_schema = None;
562 let mut number_schema = None;
563 let mut object_schema = None;
564 let mut string_schema = None;
565
566 let types: Vec<&str> = match r#type {
567 SchemaType::None => vec![],
568 SchemaType::Single(ref s) => vec![s],
569 SchemaType::Multiple(ref values) => values.iter().map(|s| s.as_ref()).collect(),
570 };
571
572 for s in types {
573 match s {
574 "array" => {
575 debug!("[Subschema#try_from] Instantiating array schema");
576 array_schema = ArraySchema::try_from(mapping).map(Some)?;
577 }
578 "boolean" => {}
580 "integer" => {
581 debug!("[Subschema#try_from] Instantiating integer schema");
582 integer_schema = IntegerSchema::try_from(mapping).map(Some)?;
583 }
584 "number" => {
585 debug!("[Subschema#try_from] Instantiating number schema");
586 number_schema = NumberSchema::try_from(mapping).map(Some)?;
587 }
588 "object" => {
589 debug!("[Subschema#try_from] Instantiating object schema");
590 object_schema = ObjectSchema::try_from(mapping).map(Some)?;
591 }
592 "string" => {
593 debug!("[Subschema#try_from] Instantiating string schema");
594 string_schema = StringSchema::try_from(mapping).map(Some)?;
595 }
596 "null" => (),
597 _ => {
598 return Err(unsupported_type!(
599 "Expected type: string, number, integer, object, array, boolean, or null, but got: {}",
600 s
601 ));
602 }
603 }
604 }
605
606 if r#type.is_none() {
607 if mapping.contains_key(&MarkedYaml::value_from_str("properties")) {
609 object_schema = ObjectSchema::try_from(mapping).map(Some)?;
611 }
612
613 if mapping.contains_key(&MarkedYaml::value_from_str("pattern"))
616 || mapping.contains_key(&MarkedYaml::value_from_str("minLength"))
617 || mapping.contains_key(&MarkedYaml::value_from_str("maxLength"))
618 {
619 r#type = SchemaType::new("string");
620 string_schema = StringSchema::try_from(mapping).map(Some)?;
621 }
622 }
623
624 let unevaluated_properties = mapping
625 .get(&MarkedYaml::value_from_str("unevaluatedProperties"))
626 .map(load_boolean_or_schema_marked)
627 .transpose()?;
628 let unevaluated_items = mapping
629 .get(&MarkedYaml::value_from_str("unevaluatedItems"))
630 .map(load_boolean_or_schema_marked)
631 .transpose()?;
632
633 debug!("[Subschema#try_from] array_schema: {array_schema:?}");
634 debug!("[Subschema#try_from] integer_schema: {integer_schema:?}");
635 debug!("[Subschema#try_from] number_schema: {number_schema:?}");
636 debug!("[Subschema#try_from] object_schema: {object_schema:?}");
637 debug!("[Subschema#try_from] string_schema: {string_schema:?}");
638
639 Ok(Self {
640 metadata_and_annotations,
641 defs,
642 r#ref: reference,
643 any_of,
644 all_of,
645 one_of,
646 not,
647 if_then_else,
648 r#type,
649 r#const,
650 r#enum,
651 array_schema,
652 integer_schema,
653 number_schema,
654 object_schema,
655 string_schema,
656 unevaluated_properties,
657 unevaluated_items,
658 anchor: None,
659 })
660 }
661}
662
663impl Display for Subschema {
664 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
665 write!(f, "{{")?;
666 if !self.metadata_and_annotations.is_empty() {
667 write!(f, " ")?;
668 self.metadata_and_annotations.fmt(f)?;
669 write!(f, " ")?;
670 }
671 if !self.r#type.is_none() {
672 write!(f, "type: ")?;
673 self.r#type.fmt(f)?;
674 }
675 if let Some(r#ref) = &self.r#ref {
676 write!(f, "$ref: ")?;
677 r#ref.fmt(f)?;
678 }
679 if let Some(defs) = &self.defs {
680 write!(f, "$defs: {}", format_linked_hash_map(defs))?;
681 }
682 if let Some(any_of) = &self.any_of {
683 write!(f, "anyOf: ")?;
684 any_of.fmt(f)?;
685 }
686 if let Some(all_of) = &self.all_of {
687 write!(f, "allOf: ")?;
688 all_of.fmt(f)?;
689 }
690 if let Some(one_of) = &self.one_of {
691 write!(f, "oneOf: ")?;
692 one_of.fmt(f)?;
693 }
694 if let Some(not) = &self.not {
695 write!(f, "not: ")?;
696 not.fmt(f)?;
697 }
698 if let Some(ite) = &self.if_then_else {
699 write!(f, "if/then/else: {ite}")?;
700 }
701 write!(f, "}}")?;
702 Ok(())
703 }
704}
705
706impl Validator for Subschema {
707 fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> crate::Result<()> {
708 debug!("[Subschema] self: {self}");
709 debug!(
710 "[Subschema] Validating value: {}",
711 format_yaml_data(&value.data)
712 );
713
714 if let Some(reference) = &self.r#ref {
715 debug!("[Subschema] Reference found: {reference}");
716 let ref_name = &reference.ref_name;
717 if let Some(root_schema) = context.root_schema {
718 if let Some(ref_path) = ref_name.strip_prefix("#") {
719 if context.is_resolving_ref(ref_name, value) {
720 context.add_error(value, format!("Circular $ref detected: {ref_name}"));
721 return Ok(());
722 }
723 let pointer = jsonptr::Pointer::parse(ref_path)?;
724 debug!("[Subschema] Pointer: {pointer}");
725 let schema = root_schema.resolve(pointer);
726 if let Some(schema) = schema {
727 debug!("[Subschema] Found {ref_path}: {schema}");
728 context.begin_resolving_ref(ref_name, value);
729 let result = schema.validate(context, value);
730 context.end_resolving_ref(ref_name, value);
731 result?;
732 } else {
733 error!("[Subschema] Cannot find definition: {ref_path}");
734 context.add_error(value, format!("Schema {ref_path} not found"));
735 }
736 } else {
737 let ref_uri = RefUri::parse(ref_name);
739 let resolved_url = if ref_uri.is_absolute() {
740 let mut url = url::Url::parse(ref_uri.base_ref()).map_err(|e| {
741 generic_error!("Failed to parse absolute $ref URI {}: {}", ref_name, e)
742 })?;
743 if let Some(frag) = ref_uri.fragment() {
744 url.set_fragment(Some(frag));
745 }
746 url
747 } else {
748 let base = root_schema.base_uri.as_ref().ok_or_else(|| {
749 generic_error!(
750 "Relative $ref requires schema to be loaded from a file or URL. Found: {}",
751 ref_name
752 )
753 })?;
754 ref_uri.resolve_against(base)?
755 };
756 let ref_key = resolved_url.to_string();
757 if context.is_resolving_ref(&ref_key, value) {
758 context.add_error(value, format!("Circular $ref detected: {ref_name}"));
759 return Ok(());
760 }
761 let doc_url = {
762 let mut u = resolved_url.clone();
763 u.set_fragment(None);
764 u.to_string()
765 };
766 let fragment = resolved_url.fragment().and_then(|f| {
767 let s = if f.starts_with('/') {
768 f.to_string()
769 } else {
770 format!("/{f}")
771 };
772 if s.is_empty() || s == "/" {
773 None
774 } else {
775 Some(s)
776 }
777 });
778 {
779 let mut schemas = context.schemas.borrow_mut();
780 if !schemas.contains_key(&doc_url) {
781 let loaded = load_external_schema(&doc_url)?;
782 let schema_rc = Rc::new(loaded);
783 let key = schema_rc.cache_key(&doc_url);
784 schemas.insert(key.clone(), Rc::clone(&schema_rc));
785 if key != doc_url {
786 schemas.insert(doc_url.clone(), schema_rc);
787 }
788 }
789 }
790 let schemas = context.schemas.borrow();
791 let schema = schemas.get(&doc_url).ok_or_else(|| {
792 generic_error!("Schema {doc_url} not in cache after load")
793 })?;
794 let pointer_opt = fragment
795 .as_ref()
796 .map(|frag| jsonptr::Pointer::parse(frag))
797 .transpose()?;
798 let target = match &pointer_opt {
799 Some(pointer) => schema.resolve(pointer),
800 None => Some(&schema.schema),
801 };
802 if let Some(target) = target {
803 context.begin_resolving_ref(&ref_key, value);
804 let result = target.validate(context, value);
805 context.end_resolving_ref(&ref_key, value);
806 result?;
807 } else {
808 error!("[Subschema] Cannot find definition: {:?}", fragment);
809 context.add_error(
810 value,
811 format!("Schema {:?} not found in {doc_url}", fragment),
812 );
813 }
814 }
815 return Ok(());
816 } else {
817 return Err(generic_error!(
818 "Subschema has a reference, but no root schema was provided!"
819 ));
820 }
821 }
822
823 let ctx = Self::validation_context_for_instance(context, value);
826
827 if let Some(any_of) = &self.any_of {
828 debug!("[Subschema] Validating anyOf schema: {any_of:?}");
829 any_of.validate(&ctx, value)?;
830 }
831
832 if let Some(all_of) = &self.all_of {
833 debug!("[Subschema] Validating allOf schema: {all_of:?}");
834 all_of.validate(&ctx, value)?;
835 }
836
837 if let Some(one_of) = &self.one_of {
838 debug!("[Subschema] Validating oneOf schema: {one_of:?}");
839 one_of.validate(&ctx, value)?;
840 }
841
842 if let Some(not) = &self.not {
843 debug!("[Subschema] Validating not schema: {not:?}");
844 not.validate(&ctx, value)?;
845 }
846
847 if let Some(if_then_else) = &self.if_then_else {
848 debug!("[Subschema] Validating if/then/else: {if_then_else:?}");
849 if_then_else.validate(&ctx, value)?;
850 }
851
852 match &self.r#type {
853 SchemaType::None => {
854 if self
855 .object_schema
856 .as_ref()
857 .is_some_and(|object_schema| object_schema.properties.is_some())
858 {
859 self.validate_by_type(&ctx, "object", value)?;
860 }
861 }
862 SchemaType::Single(s) => self.validate_by_type(&ctx, s.as_ref(), value)?,
863 SchemaType::Multiple(values) => {
864 debug!(
865 "[Subschema] Validating multiple types: {}",
866 values.join(", ")
867 );
868 let mut any_matched = false;
869 for s in values {
870 let sub_context = ctx.get_sub_context();
871 self.validate_by_type(&sub_context, s.as_ref(), value)?;
872 if !sub_context.has_errors() {
873 any_matched = true;
874 break;
875 }
876 }
877 if !any_matched {
878 ctx.add_error(
879 value,
880 format!("None of type: [{}] matched", values.join(", ")),
881 );
882 }
883 }
884 }
885
886 if let Some(r#const) = &self.r#const
887 && !r#const.accepts(value)
888 {
889 ctx.add_error(
890 value,
891 format!(
892 "Expected const: {:#?}, but got: {}",
893 r#const,
894 format_yaml_data(&value.data)
895 ),
896 );
897 }
898
899 if let Some(r#enum) = &self.r#enum {
900 debug!("[Subschema] Validating enum schema: {}", r#enum);
901 r#enum.validate(&ctx, value)?;
902 }
903
904 self.apply_unevaluated(&ctx, value)?;
905
906 Ok(())
907 }
908}
909
910impl Subschema {
911 fn validation_context_for_instance<'r>(base: &Context<'r>, value: &MarkedYaml) -> Context<'r> {
912 match &value.data {
913 YamlData::Mapping(_) => {
914 let oe = base.object_evaluated.clone().unwrap_or_default();
915 base.with_object_evaluated(Some(oe))
916 }
917 YamlData::Sequence(_) => {
918 let arr = base
919 .array_unevaluated
920 .clone()
921 .unwrap_or_else(ArrayUnevaluatedAnnotations::new_shared);
922 base.with_array_unevaluated(Some(arr))
923 }
924 _ => base
925 .with_object_evaluated(base.object_evaluated.clone())
926 .with_array_unevaluated(base.array_unevaluated.clone()),
927 }
928 }
929
930 fn apply_unevaluated(&self, ctx: &Context, value: &MarkedYaml) -> Result<()> {
931 if let YamlData::Mapping(mapping) = &value.data
932 && let Some(u) = &self.unevaluated_properties
933 {
934 let evaluated: HashSet<String> = ctx
935 .object_evaluated
936 .as_ref()
937 .map(|o| o.snapshot())
938 .unwrap_or_default();
939 for (k, v) in mapping.iter() {
940 let key_string = match &k.data {
941 YamlData::Value(scalar) => scalar_to_string(scalar),
942 _ => {
943 return Err(expected_scalar!(
944 "[{}] Expected a scalar object key, got: {:?}",
945 format_marker(&k.span.start),
946 k.data
947 ));
948 }
949 };
950 if key_string == "$schema" {
951 continue;
952 }
953 if evaluated.contains(&key_string) {
954 continue;
955 }
956 let prop_ctx = ctx.append_path(&key_string);
957 match u {
958 BooleanOrSchema::Boolean(false) => {
959 ctx.add_error(
960 v,
961 format!("Unevaluated property '{key_string}' is not allowed!"),
962 );
963 }
964 BooleanOrSchema::Boolean(true) => {}
965 BooleanOrSchema::Schema(s) => {
966 s.validate(&prop_ctx, v)?;
967 }
968 }
969 }
970 }
971
972 if let YamlData::Sequence(seq) = &value.data
973 && let Some(u) = &self.unevaluated_items
974 {
975 let ann = ctx
976 .array_unevaluated
977 .as_ref()
978 .map(|c| c.borrow().clone())
979 .unwrap_or_default();
980 if ann.full_coverage {
981 return Ok(());
982 }
983 let indices = ann.indices_requiring_unevaluated(seq.len());
984 let err_before = ctx.errors.borrow().len();
985 for i in indices.iter().copied() {
986 let item = &seq[i];
987 let item_ctx = ctx.append_path(i.to_string());
988 match u {
989 BooleanOrSchema::Boolean(false) => {
990 ctx.add_error(
991 item,
992 format!("Unevaluated array item at index {i} is not allowed!"),
993 );
994 }
995 BooleanOrSchema::Boolean(true) => {}
996 BooleanOrSchema::Schema(s) => {
997 s.validate(&item_ctx, item)?;
998 }
999 }
1000 }
1001 if ctx.errors.borrow().len() == err_before
1002 && !indices.is_empty()
1003 && let Some(cell) = &ctx.array_unevaluated
1004 {
1005 let mut a = cell.borrow_mut();
1006 a.saw_relevant = true;
1007 a.full_coverage = true;
1008 }
1009 }
1010
1011 Ok(())
1012 }
1013
1014 fn validate_by_type(
1015 &self,
1016 context: &Context,
1017 r#type: &str,
1018 value: &saphyr::MarkedYaml,
1019 ) -> Result<()> {
1020 debug!("[Subschema#validate_by_type] r#type: {}", r#type);
1021 match r#type {
1022 "array" => {
1023 if let Some(array_schema) = &self.array_schema {
1024 debug!("[Subschema] Validating array schema: {array_schema:?}");
1025 array_schema.validate(context, value)?;
1026 } else {
1027 error!("[Subschema#validate_by_type] No array schema found");
1028 context.add_error(value, format!("No array schema found for type: {}", r#type));
1029 }
1030 }
1031 "boolean" => {
1032 if !matches!(&value.data, YamlData::Value(Scalar::Boolean(_))) {
1033 context.add_error(
1034 value,
1035 format!(
1036 "Expected boolean, but got: {}",
1037 format_yaml_data(&value.data)
1038 ),
1039 );
1040 }
1041 }
1042 "null" => {
1043 if !matches!(&value.data, YamlData::Value(Scalar::Null)) {
1044 context.add_error(
1045 value,
1046 format!("Expected null, but got: {}", format_yaml_data(&value.data)),
1047 );
1048 }
1049 }
1050 "string" => {
1051 if let Some(string_schema) = &self.string_schema {
1052 debug!("[Subschema] Validating string schema: {string_schema:?}");
1053 string_schema.validate(context, value)?;
1054 } else {
1055 error!("[Subschema#validate_by_type] No string schema found");
1056 context.add_error(
1057 value,
1058 format!("No string schema found for type: {}", r#type),
1059 );
1060 }
1061 }
1062 "number" => {
1063 if let Some(number_schema) = &self.number_schema {
1064 debug!("[Subschema] Validating number schema: {number_schema:?}");
1065 number_schema.validate(context, value)?;
1066 } else {
1067 error!("[Subschema#validate_by_type] No number schema found");
1068 context.add_error(
1069 value,
1070 format!("No number schema found for type: {}", r#type),
1071 );
1072 }
1073 }
1074 "integer" => {
1075 if let Some(integer_schema) = &self.integer_schema {
1076 debug!("[Subschema] Validating integer schema: {integer_schema:?}");
1077 integer_schema.validate(context, value)?;
1078 } else {
1079 error!("[Subschema#validate_by_type] No integer schema found");
1080 context.add_error(
1081 value,
1082 format!("No integer schema found for type: {}", r#type),
1083 );
1084 }
1085 }
1086 "object" => {
1087 if let Some(object_schema) = &self.object_schema {
1088 debug!("[Subschema] Validating object schema: {object_schema:?}");
1089 object_schema.validate(context, value)?;
1090 } else {
1091 error!("[Subschema#validate_by_type] No object schema found");
1092 context.add_error(
1093 value,
1094 format!("No object schema found for type: {}", r#type),
1095 );
1096 }
1097 }
1098 _ => {
1099 error!("[Subschema#validate_by_type] Unsupported type: {}", r#type);
1100 context.add_error(value, format!("Unsupported type: {}", r#type));
1101 }
1102 }
1103 Ok(())
1104 }
1105}
1106
1107#[derive(Debug, Default, PartialEq)]
1109pub struct MetadataAndAnnotations {
1110 pub id: Option<String>,
1112 pub schema: Option<String>,
1114 pub title: Option<String>,
1116 pub description: Option<String>,
1118}
1119
1120impl MetadataAndAnnotations {
1121 pub fn is_empty(&self) -> bool {
1122 self.id.is_none()
1123 && self.schema.is_none()
1124 && self.title.is_none()
1125 && self.description.is_none()
1126 }
1127}
1128
1129impl std::fmt::Display for MetadataAndAnnotations {
1130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1131 write!(f, "{{")?;
1132 if !self.is_empty() {
1133 write!(f, " ")?;
1134 if let Some(id) = &self.id {
1135 write!(f, "id: {id}, ")?;
1136 }
1137 if let Some(schema) = &self.schema {
1138 write!(f, "schema: {schema}, ")?;
1139 }
1140 if let Some(title) = &self.title {
1141 write!(f, "title: {title}, ")?;
1142 }
1143 if let Some(description) = &self.description {
1144 write!(f, "description: {description}, ")?;
1145 }
1146 write!(f, " ")?;
1147 }
1148 write!(f, "}}")?;
1149 Ok(())
1150 }
1151}
1152
1153impl TryFrom<&AnnotatedMapping<'_, MarkedYaml<'_>>> for MetadataAndAnnotations {
1154 type Error = Error;
1155
1156 fn try_from(mapping: &AnnotatedMapping<'_, MarkedYaml<'_>>) -> crate::Result<Self> {
1157 let mut metadata_and_annotations = MetadataAndAnnotations::default();
1158 for (key, value) in mapping.iter() {
1159 match &key.data {
1160 YamlData::Value(Scalar::String(s)) => match s.as_ref() {
1161 "$id" => {
1162 metadata_and_annotations.id =
1163 Some(marked_yaml_to_string(value, "$id must be a string")?);
1164 }
1165 "$schema" => {
1166 metadata_and_annotations.schema =
1167 Some(marked_yaml_to_string(value, "$schema must be a string")?);
1168 }
1169 "title" => {
1170 metadata_and_annotations.title =
1171 Some(marked_yaml_to_string(value, "title must be a string")?);
1172 }
1173 "description" => {
1174 metadata_and_annotations.description = Some(marked_yaml_to_string(
1175 value,
1176 "description must be a string",
1177 )?);
1178 }
1179 _ => {
1180 debug!("[MetadataAndAnnotations#try_from] Unknown key: {s}");
1181 }
1182 },
1183 _ => {
1184 debug!("[MetadataAndAnnotations#try_from] Unsupported key data: {key:?}");
1185 }
1186 }
1187 }
1188 Ok(metadata_and_annotations)
1189 }
1190}
1191
1192#[cfg(test)]
1193mod tests {
1194 use saphyr::LoadableYamlNode;
1195
1196 use crate::engine;
1197 use crate::loader;
1198
1199 use super::*;
1200
1201 #[test]
1202 fn test_type_boolean() {
1203 let yaml = r#"
1204 type: boolean
1205 "#;
1206 let doc = MarkedYaml::load_from_str(yaml).expect("Failed to load YAML");
1207 let marked_yaml = doc.first().unwrap();
1208 let yaml_schema = YamlSchema::try_from(marked_yaml).unwrap();
1209 let YamlSchema::Subschema(subschema) = yaml_schema else {
1210 panic!("Expected a subschema");
1211 };
1212 assert!(!subschema.r#type.is_none());
1213 assert!(subschema.r#type.is_single());
1214 let SchemaType::Single(type_value) = subschema.r#type else {
1215 panic!("Expected a single type");
1216 };
1217 assert_eq!(type_value, "boolean");
1218 }
1219
1220 #[test]
1221 fn test_metadata_and_annotations_try_from() {
1222 let yaml = r#"
1223 $id: http://example.com/schema
1224 $schema: http://example.com/schema
1225 title: Example Schema
1226 description: This is an example schema
1227 "#;
1228 let doc = MarkedYaml::load_from_str(yaml).expect("Failed to load YAML");
1229 let marked_yaml = doc.first().unwrap();
1230 assert!(marked_yaml.data.is_mapping());
1231 let YamlData::Mapping(mapping) = &marked_yaml.data else {
1232 panic!("Expected a mapping");
1233 };
1234 let metadata_and_annotations = MetadataAndAnnotations::try_from(mapping).unwrap();
1235 assert_eq!(
1236 metadata_and_annotations.id,
1237 Some("http://example.com/schema".to_string())
1238 );
1239 assert_eq!(
1240 metadata_and_annotations.schema,
1241 Some("http://example.com/schema".to_string())
1242 );
1243 assert_eq!(
1244 metadata_and_annotations.title,
1245 Some("Example Schema".to_string())
1246 );
1247 assert_eq!(
1248 metadata_and_annotations.description,
1249 Some("This is an example schema".to_string())
1250 );
1251 }
1252
1253 #[test]
1254 fn test_yaml_schema_with_multiple_types() {
1255 let yaml = r#"
1256 type:
1257 - boolean
1258 - number
1259 - integer
1260 - string
1261 "#;
1262 let doc = MarkedYaml::load_from_str(yaml).expect("Failed to load YAML");
1263 let marked_yaml = doc.first().unwrap();
1264 let yaml_schema = YamlSchema::try_from(marked_yaml).unwrap();
1265 let YamlSchema::Subschema(subschema) = yaml_schema else {
1266 panic!("Expected a subschema");
1267 };
1268 assert!(!subschema.r#type.is_none());
1269 assert!(subschema.r#type.is_multiple());
1270 let SchemaType::Multiple(type_values) = subschema.r#type else {
1271 panic!("Expected a multiple type");
1272 };
1273 assert_eq!(type_values, vec!["boolean", "number", "integer", "string"]);
1274 }
1275
1276 #[test]
1277 fn test_multiple_types() {
1278 let schema = r#"
1279 type:
1280 - string
1281 - number
1282 "#;
1283 let schema = loader::load_from_str(schema).unwrap();
1284
1285 let s = "I'm a string";
1286 let docs = MarkedYaml::load_from_str(s).unwrap();
1287 let value = docs.first().unwrap();
1288 let context = Context::default();
1289 let result = schema.validate(&context, value);
1290 assert!(result.is_ok());
1291 assert!(!context.has_errors());
1292
1293 let s = "42";
1294 let docs = MarkedYaml::load_from_str(s).unwrap();
1295 let value = docs.first().unwrap();
1296 let context = Context::default();
1297 let result = schema.validate(&context, value);
1298 assert!(result.is_ok());
1299 assert!(!context.has_errors());
1300
1301 let s = "null";
1302 let docs = MarkedYaml::load_from_str(s).unwrap();
1303 let value = docs.first().unwrap();
1304 let context = Context::default();
1305 let result = schema.validate(&context, value);
1306 assert!(result.is_ok());
1307 assert!(context.has_errors());
1308 let errors = context.errors.borrow();
1309 assert_eq!(errors.len(), 1);
1310 assert_eq!(errors[0].error, "None of type: [string, number] matched");
1311 }
1312
1313 #[test]
1314 fn properties_without_type_infers_object_and_validates() {
1315 let yaml = r#"
1316 properties:
1317 foo:
1318 type: string
1319 required:
1320 - foo
1321 "#;
1322 let root = loader::load_from_str(yaml).unwrap();
1323 let YamlSchema::Subschema(sub) = &root.schema else {
1324 panic!("expected subschema");
1325 };
1326 assert!(sub.r#type.is_none(), "expected type: none");
1327 assert!(sub.object_schema.is_some());
1328
1329 let ok = engine::Engine::evaluate(&root, "foo: bar", false).unwrap();
1330 assert!(!ok.has_errors());
1331
1332 let bad = engine::Engine::evaluate(&root, "other: x", false).unwrap();
1333 assert!(bad.has_errors());
1334 }
1335
1336 #[test]
1337 fn test_object_schema_with_const_property() {
1338 let schema = r#"
1339 type: object
1340 properties:
1341 const:
1342 description: A scalar value that must match the value
1343 type:
1344 - string
1345 - integer
1346 - number
1347 - boolean
1348 "#;
1349 let schema = loader::load_from_str(schema).expect("Failed to load schema");
1350
1351 let docs = MarkedYaml::load_from_str(
1352 r#"
1353 const: "I'm a string"
1354 "#,
1355 )
1356 .unwrap();
1357 let value = docs.first().unwrap();
1358 let context = Context::default();
1359 let result = schema.validate(&context, value);
1360 assert!(result.is_ok());
1361 assert!(!context.has_errors());
1362 }
1363
1364 #[test]
1365 fn unevaluated_properties_all_of_extra_key_rejected() {
1366 let root = loader::load_from_str(
1367 r#"
1368 allOf:
1369 - properties:
1370 a:
1371 type: string
1372 - unevaluatedProperties: false
1373 "#,
1374 )
1375 .unwrap();
1376 let ok = engine::Engine::evaluate(&root, "a: ok", false).unwrap();
1377 assert!(!ok.has_errors());
1378 let bad = engine::Engine::evaluate(&root, "a: ok\nb: no", false).unwrap();
1379 assert!(bad.has_errors());
1380 }
1381}