1use std::collections::HashSet;
6
7use styx_tree::{Payload, Value};
8
9fn levenshtein(a: &str, b: &str) -> usize {
11 let a_len = a.chars().count();
12 let b_len = b.chars().count();
13
14 if a_len == 0 {
15 return b_len;
16 }
17 if b_len == 0 {
18 return a_len;
19 }
20
21 let mut prev_row: Vec<usize> = (0..=b_len).collect();
22 let mut curr_row = vec![0; b_len + 1];
23
24 for (i, a_char) in a.chars().enumerate() {
25 curr_row[0] = i + 1;
26 for (j, b_char) in b.chars().enumerate() {
27 let cost = if a_char == b_char { 0 } else { 1 };
28 curr_row[j + 1] = (prev_row[j + 1] + 1)
29 .min(curr_row[j] + 1)
30 .min(prev_row[j] + cost);
31 }
32 std::mem::swap(&mut prev_row, &mut curr_row);
33 }
34
35 prev_row[b_len]
36}
37
38fn suggest_similar<'a>(unknown: &str, valid: &'a [String]) -> Option<&'a str> {
40 let unknown_lower = unknown.to_lowercase();
41 valid
42 .iter()
43 .filter_map(|v| {
44 let v_lower = v.to_lowercase();
45 let dist = levenshtein(&unknown_lower, &v_lower);
46 if dist <= 2 && dist < unknown.len().max(1) {
48 Some((v.as_str(), dist))
49 } else {
50 None
51 }
52 })
53 .min_by_key(|(_, d)| *d)
54 .map(|(v, _)| v)
55}
56
57use crate::schema_error::{
58 ValidationError, ValidationErrorKind, ValidationResult, ValidationWarning,
59 ValidationWarningKind,
60};
61use crate::schema_types::{
62 DefaultSchema, DeprecatedSchema, Documented, EnumSchema, FlattenSchema, FloatConstraints,
63 IntConstraints, MapSchema, ObjectKey, ObjectSchema, OneOfSchema, OptionalSchema, Schema,
64 SchemaFile, SeqSchema, StringConstraints, TupleSchema, UnionSchema,
65};
66
67pub struct Validator<'a> {
69 schema_file: &'a SchemaFile,
71}
72
73impl<'a> Validator<'a> {
74 pub fn new(schema_file: &'a SchemaFile) -> Self {
76 Self { schema_file }
77 }
78
79 pub fn validate_document(&self, doc: &Value) -> ValidationResult {
81 match self.schema_file.schema.get(&None) {
83 Some(root_schema) => self.validate_value(doc, root_schema, ""),
84 None => {
85 let mut result = ValidationResult::ok();
86 result.error(
87 ValidationError::new(
88 "",
89 ValidationErrorKind::SchemaError {
90 reason: "no root type (@) defined in schema".into(),
91 },
92 "schema has no root type definition",
93 )
94 .with_span(doc.span),
95 );
96 result
97 }
98 }
99 }
100
101 pub fn validate_as_type(&self, value: &Value, type_name: &str) -> ValidationResult {
103 match self.schema_file.schema.get(&Some(type_name.to_string())) {
104 Some(schema) => self.validate_value(value, schema, ""),
105 None => {
106 let mut result = ValidationResult::ok();
107 result.error(
108 ValidationError::new(
109 "",
110 ValidationErrorKind::UnknownType {
111 name: type_name.into(),
112 },
113 format!("unknown type '{type_name}'"),
114 )
115 .with_span(value.span),
116 );
117 result
118 }
119 }
120 }
121
122 pub fn validate_value(&self, value: &Value, schema: &Schema, path: &str) -> ValidationResult {
124 match schema {
125 Schema::String(constraints) => self.validate_string(value, constraints.as_ref(), path),
127 Schema::Int(constraints) => self.validate_int(value, constraints.as_ref(), path),
128 Schema::Float(constraints) => self.validate_float(value, constraints.as_ref(), path),
129 Schema::Bool => self.validate_bool(value, path),
130 Schema::Unit => self.validate_unit(value, path),
131 Schema::Any => ValidationResult::ok(),
132
133 Schema::Object(obj_schema) => self.validate_object(value, obj_schema, path),
135 Schema::Seq(seq_schema) => self.validate_seq(value, seq_schema, path),
136 Schema::Tuple(tuple_schema) => self.validate_tuple(value, tuple_schema, path),
137 Schema::Map(map_schema) => self.validate_map(value, map_schema, path),
138
139 Schema::Union(union_schema) => self.validate_union(value, union_schema, path),
141 Schema::Optional(opt_schema) => self.validate_optional(value, opt_schema, path),
142 Schema::Enum(enum_schema) => self.validate_enum(value, enum_schema, path),
143 Schema::OneOf(oneof_schema) => self.validate_one_of(value, oneof_schema, path),
144 Schema::Flatten(flatten_schema) => self.validate_flatten(value, flatten_schema, path),
145
146 Schema::Default(default_schema) => self.validate_default(value, default_schema, path),
148 Schema::Deprecated(deprecated_schema) => {
149 self.validate_deprecated(value, deprecated_schema, path)
150 }
151
152 Schema::Literal(expected) => self.validate_literal(value, expected, path),
154 Schema::Type { name } => self.validate_type_ref(value, name.as_deref(), path),
155 }
156 }
157
158 fn validate_string(
163 &self,
164 value: &Value,
165 constraints: Option<&StringConstraints>,
166 path: &str,
167 ) -> ValidationResult {
168 let mut result = ValidationResult::ok();
169
170 let text = match value.scalar_text() {
171 Some(t) => t,
172 None => {
173 result.error(
174 ValidationError::new(
175 path,
176 ValidationErrorKind::ExpectedScalar,
177 format!("expected string, got {}", value_type_name(value)),
178 )
179 .with_span(value.span),
180 );
181 return result;
182 }
183 };
184
185 if let Some(c) = constraints {
187 if let Some(min) = c.min_len
188 && text.len() < min
189 {
190 result.error(
191 ValidationError::new(
192 path,
193 ValidationErrorKind::InvalidValue {
194 reason: format!("string length {} < minimum {}", text.len(), min),
195 },
196 format!("string too short (min length: {})", min),
197 )
198 .with_span(value.span),
199 );
200 }
201 if let Some(max) = c.max_len
202 && text.len() > max
203 {
204 result.error(
205 ValidationError::new(
206 path,
207 ValidationErrorKind::InvalidValue {
208 reason: format!("string length {} > maximum {}", text.len(), max),
209 },
210 format!("string too long (max length: {})", max),
211 )
212 .with_span(value.span),
213 );
214 }
215 if let Some(pattern) = &c.pattern {
216 let _ = pattern; }
219 }
220
221 result
222 }
223
224 fn validate_int(
225 &self,
226 value: &Value,
227 constraints: Option<&IntConstraints>,
228 path: &str,
229 ) -> ValidationResult {
230 let mut result = ValidationResult::ok();
231
232 let text = match value.scalar_text() {
233 Some(t) => t,
234 None => {
235 result.error(
236 ValidationError::new(
237 path,
238 ValidationErrorKind::ExpectedScalar,
239 format!("expected integer, got {}", value_type_name(value)),
240 )
241 .with_span(value.span),
242 );
243 return result;
244 }
245 };
246
247 let parsed = match text.parse::<i128>() {
248 Ok(n) => n,
249 Err(_) => {
250 result.error(
251 ValidationError::new(
252 path,
253 ValidationErrorKind::InvalidValue {
254 reason: "not a valid integer".into(),
255 },
256 format!("'{}' is not a valid integer", text),
257 )
258 .with_span(value.span),
259 );
260 return result;
261 }
262 };
263
264 if let Some(c) = constraints {
266 if let Some(min) = c.min
267 && parsed < min
268 {
269 result.error(
270 ValidationError::new(
271 path,
272 ValidationErrorKind::InvalidValue {
273 reason: format!("value {} < minimum {}", parsed, min),
274 },
275 format!("value too small (min: {})", min),
276 )
277 .with_span(value.span),
278 );
279 }
280 if let Some(max) = c.max
281 && parsed > max
282 {
283 result.error(
284 ValidationError::new(
285 path,
286 ValidationErrorKind::InvalidValue {
287 reason: format!("value {} > maximum {}", parsed, max),
288 },
289 format!("value too large (max: {})", max),
290 )
291 .with_span(value.span),
292 );
293 }
294 }
295
296 result
297 }
298
299 fn validate_float(
300 &self,
301 value: &Value,
302 constraints: Option<&FloatConstraints>,
303 path: &str,
304 ) -> ValidationResult {
305 let mut result = ValidationResult::ok();
306
307 let text = match value.scalar_text() {
308 Some(t) => t,
309 None => {
310 result.error(
311 ValidationError::new(
312 path,
313 ValidationErrorKind::ExpectedScalar,
314 format!("expected number, got {}", value_type_name(value)),
315 )
316 .with_span(value.span),
317 );
318 return result;
319 }
320 };
321
322 let parsed = match text.parse::<f64>() {
323 Ok(n) => n,
324 Err(_) => {
325 result.error(
326 ValidationError::new(
327 path,
328 ValidationErrorKind::InvalidValue {
329 reason: "not a valid number".into(),
330 },
331 format!("'{}' is not a valid number", text),
332 )
333 .with_span(value.span),
334 );
335 return result;
336 }
337 };
338
339 if let Some(c) = constraints {
341 if let Some(min) = c.min
342 && parsed < min
343 {
344 result.error(
345 ValidationError::new(
346 path,
347 ValidationErrorKind::InvalidValue {
348 reason: format!("value {} < minimum {}", parsed, min),
349 },
350 format!("value too small (min: {})", min),
351 )
352 .with_span(value.span),
353 );
354 }
355 if let Some(max) = c.max
356 && parsed > max
357 {
358 result.error(
359 ValidationError::new(
360 path,
361 ValidationErrorKind::InvalidValue {
362 reason: format!("value {} > maximum {}", parsed, max),
363 },
364 format!("value too large (max: {})", max),
365 )
366 .with_span(value.span),
367 );
368 }
369 }
370
371 result
372 }
373
374 fn validate_bool(&self, value: &Value, path: &str) -> ValidationResult {
375 let mut result = ValidationResult::ok();
376
377 match value.scalar_text() {
378 Some(text) if text == "true" || text == "false" => {}
379 Some(text) => {
380 result.error(
381 ValidationError::new(
382 path,
383 ValidationErrorKind::InvalidValue {
384 reason: "not a valid boolean".into(),
385 },
386 format!("'{}' is not a valid boolean (expected true/false)", text),
387 )
388 .with_span(value.span),
389 );
390 }
391 None => {
392 result.error(
393 ValidationError::new(
394 path,
395 ValidationErrorKind::ExpectedScalar,
396 format!("expected boolean, got {}", value_type_name(value)),
397 )
398 .with_span(value.span),
399 );
400 }
401 }
402
403 result
404 }
405
406 fn validate_unit(&self, value: &Value, path: &str) -> ValidationResult {
407 let mut result = ValidationResult::ok();
408
409 if !value.is_unit() {
410 result.error(
411 ValidationError::new(
412 path,
413 ValidationErrorKind::TypeMismatch {
414 expected: "unit".into(),
415 got: value_type_name(value).into(),
416 },
417 "expected unit value",
418 )
419 .with_span(value.span),
420 );
421 }
422
423 result
424 }
425
426 fn validate_object(
431 &self,
432 value: &Value,
433 schema: &ObjectSchema,
434 path: &str,
435 ) -> ValidationResult {
436 let mut result = ValidationResult::ok();
437
438 let obj = match value.as_object() {
439 Some(o) => o,
440 None => {
441 result.error(
442 ValidationError::new(
443 path,
444 ValidationErrorKind::ExpectedObject,
445 format!("expected object, got {}", value_type_name(value)),
446 )
447 .with_span(value.span),
448 );
449 return result;
450 }
451 };
452
453 let mut seen_fields: HashSet<Option<&str>> = HashSet::new();
454 let additional_schema = schema
456 .0
457 .iter()
458 .find_map(|(k, v)| if k.value.tag.is_some() { Some(v) } else { None });
459
460 for entry in &obj.entries {
461 let key_opt: Option<&str> = if entry.key.is_unit() {
462 None
463 } else if let Some(s) = entry.key.as_str() {
464 Some(s)
465 } else {
466 result.error(
467 ValidationError::new(
468 path,
469 ValidationErrorKind::InvalidValue {
470 reason: "object keys must be scalars or unit".into(),
471 },
472 "invalid object key",
473 )
474 .with_span(entry.key.span),
475 );
476 continue;
477 };
478
479 let key_display = key_opt.unwrap_or("@");
480 let field_path = if path.is_empty() {
481 key_display.to_string()
482 } else {
483 format!("{path}.{key_display}")
484 };
485
486 seen_fields.insert(key_opt);
487
488 let lookup_key = Documented::new(ObjectKey::named(key_opt.unwrap_or("")));
490 if let Some(field_schema) = schema.0.get(&lookup_key) {
491 result.merge(self.validate_value(&entry.value, field_schema, &field_path));
492 } else if let Some(add_schema) = additional_schema {
493 result.merge(self.validate_value(&entry.value, add_schema, &field_path));
494 } else {
495 let valid_fields: Vec<String> = schema
497 .0
498 .keys()
499 .filter_map(|k| k.value.name().map(|s| s.to_string()))
500 .collect();
501
502 let suggestion = suggest_similar(key_display, &valid_fields).map(String::from);
504
505 result.error(
506 ValidationError::new(
507 &field_path,
508 ValidationErrorKind::UnknownField {
509 field: key_display.into(),
510 valid_fields,
511 suggestion,
512 },
513 format!("unknown field '{key_display}'"),
514 )
515 .with_span(entry.key.span),
516 );
517 }
518 }
519
520 for (field_name_doc, field_schema) in &schema.0 {
522 let Some(name) = field_name_doc.value.name() else {
524 continue;
525 };
526
527 if !seen_fields.contains(&Some(name)) {
528 if !matches!(field_schema, Schema::Optional(_) | Schema::Default(_)) {
530 let field_path = if path.is_empty() {
531 name.to_string()
532 } else {
533 format!("{path}.{name}")
534 };
535 result.error(
536 ValidationError::new(
537 &field_path,
538 ValidationErrorKind::MissingField {
539 field: name.to_string(),
540 },
541 format!("missing required field '{name}'"),
542 )
543 .with_span(value.span),
544 );
545 }
546 }
547 }
548
549 result
550 }
551
552 fn validate_seq(&self, value: &Value, schema: &SeqSchema, path: &str) -> ValidationResult {
553 let mut result = ValidationResult::ok();
554
555 let seq = match value.as_sequence() {
556 Some(s) => s,
557 None => {
558 result.error(
559 ValidationError::new(
560 path,
561 ValidationErrorKind::ExpectedSequence,
562 format!("expected sequence, got {}", value_type_name(value)),
563 )
564 .with_span(value.span),
565 );
566 return result;
567 }
568 };
569
570 let inner_schema = &*schema.0.0.value;
572 for (i, item) in seq.items.iter().enumerate() {
573 let item_path = format!("{path}[{i}]");
574 result.merge(self.validate_value(item, inner_schema, &item_path));
575 }
576
577 result
578 }
579
580 fn validate_tuple(&self, value: &Value, schema: &TupleSchema, path: &str) -> ValidationResult {
581 let mut result = ValidationResult::ok();
582
583 let seq = match value.as_sequence() {
584 Some(s) => s,
585 None => {
586 result.error(
587 ValidationError::new(
588 path,
589 ValidationErrorKind::ExpectedSequence,
590 format!("expected tuple (sequence), got {}", value_type_name(value)),
591 )
592 .with_span(value.span),
593 );
594 return result;
595 }
596 };
597
598 let expected_len = schema.0.len();
599 let actual_len = seq.items.len();
600
601 if actual_len != expected_len {
602 result.error(
603 ValidationError::new(
604 path,
605 ValidationErrorKind::InvalidValue {
606 reason: format!(
607 "tuple has wrong number of elements: expected {}, got {}",
608 expected_len, actual_len
609 ),
610 },
611 format!(
612 "tuple has wrong number of elements: expected {}, got {}",
613 expected_len, actual_len
614 ),
615 )
616 .with_span(value.span),
617 );
618 }
620
621 for (i, (item, element_schema)) in seq.items.iter().zip(schema.0.iter()).enumerate() {
623 let item_path = format!("{path}[{i}]");
624 result.merge(self.validate_value(item, &element_schema.value, &item_path));
625 }
626
627 result
628 }
629
630 fn validate_map(&self, value: &Value, schema: &MapSchema, path: &str) -> ValidationResult {
631 let mut result = ValidationResult::ok();
632
633 let obj = match value.as_object() {
634 Some(o) => o,
635 None => {
636 result.error(
637 ValidationError::new(
638 path,
639 ValidationErrorKind::ExpectedObject,
640 format!("expected map (object), got {}", value_type_name(value)),
641 )
642 .with_span(value.span),
643 );
644 return result;
645 }
646 };
647
648 let (key_schema, value_schema) = match schema.0.len() {
650 1 => (None, &schema.0[0].value),
651 2 => (Some(&schema.0[0].value), &schema.0[1].value),
652 n => {
653 result.error(
654 ValidationError::new(
655 path,
656 ValidationErrorKind::SchemaError {
657 reason: format!("map schema must have 1 or 2 types, got {}", n),
658 },
659 "invalid map schema",
660 )
661 .with_span(value.span),
662 );
663 return result;
664 }
665 };
666
667 for entry in &obj.entries {
668 let key_str = match entry.key.as_str() {
669 Some(s) => s,
670 None => {
671 result.error(
672 ValidationError::new(
673 path,
674 ValidationErrorKind::InvalidValue {
675 reason: "map keys must be scalars".into(),
676 },
677 "invalid map key",
678 )
679 .with_span(entry.key.span),
680 );
681 continue;
682 }
683 };
684
685 if let Some(ks) = key_schema {
687 result.merge(self.validate_value(&entry.key, ks, path));
688 }
689
690 let entry_path = if path.is_empty() {
692 key_str.to_string()
693 } else {
694 format!("{path}.{key_str}")
695 };
696 result.merge(self.validate_value(&entry.value, value_schema, &entry_path));
697 }
698
699 result
700 }
701
702 fn validate_union(&self, value: &Value, schema: &UnionSchema, path: &str) -> ValidationResult {
707 let mut result = ValidationResult::ok();
708
709 if schema.0.is_empty() {
710 result.error(
711 ValidationError::new(
712 path,
713 ValidationErrorKind::SchemaError {
714 reason: "union must have at least one variant".into(),
715 },
716 "invalid union schema: no variants",
717 )
718 .with_span(value.span),
719 );
720 return result;
721 }
722
723 let mut tried = Vec::new();
724 for variant in &schema.0 {
725 let variant_result = self.validate_value(value, &variant.value, path);
726 if variant_result.is_valid() {
727 return ValidationResult::ok();
728 }
729 tried.push(schema_type_name(&variant.value));
730 }
731
732 result.error(
733 ValidationError::new(
734 path,
735 ValidationErrorKind::UnionMismatch { tried },
736 format!(
737 "value doesn't match any union variant (tried: {})",
738 schema
739 .0
740 .iter()
741 .map(|d| schema_type_name(&d.value))
742 .collect::<Vec<_>>()
743 .join(", ")
744 ),
745 )
746 .with_span(value.span),
747 );
748
749 result
750 }
751
752 fn validate_optional(
753 &self,
754 value: &Value,
755 schema: &OptionalSchema,
756 path: &str,
757 ) -> ValidationResult {
758 if value.is_unit() {
760 return ValidationResult::ok();
761 }
762 self.validate_value(value, &schema.0.0.value, path)
764 }
765
766 fn validate_enum(&self, value: &Value, schema: &EnumSchema, path: &str) -> ValidationResult {
767 let mut result = ValidationResult::ok();
768
769 let tag = match &value.tag {
771 Some(t) => t.name.as_str(),
772 None => {
773 if let Some(fallback_schema) = self.find_enum_fallback(value, schema) {
775 return self.validate_value(value, fallback_schema, path);
777 }
778 result.error(
779 ValidationError::new(
780 path,
781 ValidationErrorKind::ExpectedTagged,
782 format!(
783 "expected tagged value for enum, got {}",
784 value_type_name(value)
785 ),
786 )
787 .with_span(value.span),
788 );
789 return result;
790 }
791 };
792
793 let payload_value = value.payload.as_ref().map(|p| Value {
795 tag: None,
796 payload: Some(p.clone()),
797 span: None,
798 });
799
800 let expected_variants: Vec<String> = schema.0.keys().map(|k| k.value.clone()).collect();
801
802 match schema.0.get(&Documented::new(tag.to_string())) {
803 Some(variant_schema) => {
804 match (&payload_value, variant_schema) {
805 (None, Schema::Unit) => {
806 }
808 (None, Schema::Type { name: Some(n) }) if n == "unit" => {
809 }
811 (None, Schema::Type { name: None }) => {
812 }
814 (Some(p), _) => {
815 let variant_path = if path.is_empty() {
816 tag.to_string()
817 } else {
818 format!("{path}.{tag}")
819 };
820 result.merge(self.validate_value(p, variant_schema, &variant_path));
821 }
822 (None, _) => {
823 result.error(
824 ValidationError::new(
825 path,
826 ValidationErrorKind::TypeMismatch {
827 expected: schema_type_name(variant_schema),
828 got: "unit".into(),
829 },
830 format!("variant '{tag}' requires a payload"),
831 )
832 .with_span(value.span),
833 );
834 }
835 }
836 }
837 None => {
838 result.error(
839 ValidationError::new(
840 path,
841 ValidationErrorKind::InvalidVariant {
842 expected: expected_variants.clone(),
843 got: tag.into(),
844 },
845 format!(
846 "unknown enum variant '{tag}' (expected one of: {})",
847 expected_variants.join(", ")
848 ),
849 )
850 .with_span(value.span),
851 );
852 }
853 }
854
855 result
856 }
857
858 fn find_enum_fallback<'s>(&self, value: &Value, schema: &'s EnumSchema) -> Option<&'s Schema> {
863 let text = value.scalar_text()?;
865
866 for variant_schema in schema.0.values() {
868 match variant_schema {
869 Schema::String(_) => return Some(variant_schema),
871 Schema::Int(_) if text.parse::<i64>().is_ok() => return Some(variant_schema),
873 Schema::Float(_) if text.parse::<f64>().is_ok() => return Some(variant_schema),
875 Schema::Bool if text == "true" || text == "false" => return Some(variant_schema),
877 _ => continue,
878 }
879 }
880
881 None
882 }
883
884 fn validate_one_of(&self, value: &Value, schema: &OneOfSchema, path: &str) -> ValidationResult {
885 let mut result = ValidationResult::ok();
886
887 let base_type = &schema.0.0.value;
889 let base_result = self.validate_value(value, base_type, path);
890 if !base_result.is_valid() {
891 return base_result;
892 }
893
894 let allowed_values = &schema.0.1;
896 if allowed_values.is_empty() {
897 return result;
899 }
900
901 let value_text = match value.scalar_text() {
903 Some(t) => t,
904 None => {
905 result.error(
907 ValidationError::new(
908 path,
909 ValidationErrorKind::ExpectedScalar,
910 format!(
911 "expected scalar value for one-of constraint, got {}",
912 value_type_name(value)
913 ),
914 )
915 .with_span(value.span),
916 );
917 return result;
918 }
919 };
920
921 let allowed_strings: Vec<&str> = allowed_values.iter().map(|v| v.as_str()).collect();
923 if !allowed_strings.contains(&value_text) {
924 let allowed_owned: Vec<String> = allowed_values.iter().map(|v| v.0.clone()).collect();
926 let suggestion = suggest_similar(value_text, &allowed_owned).map(String::from);
927
928 result.error(
929 ValidationError::new(
930 path,
931 ValidationErrorKind::InvalidValue {
932 reason: format!(
933 "value '{}' not in allowed set: {}",
934 value_text,
935 allowed_strings.join(", ")
936 ),
937 },
938 format!(
939 "'{}' is not one of: {}{}",
940 value_text,
941 allowed_strings.join(", "),
942 suggestion
943 .map(|s| format!(" (did you mean '{}'?)", s))
944 .unwrap_or_default()
945 ),
946 )
947 .with_span(value.span),
948 );
949 }
950
951 result
952 }
953
954 fn validate_flatten(
955 &self,
956 value: &Value,
957 schema: &FlattenSchema,
958 path: &str,
959 ) -> ValidationResult {
960 self.validate_value(value, &schema.0.0.value, path)
962 }
963
964 fn validate_default(
969 &self,
970 value: &Value,
971 schema: &DefaultSchema,
972 path: &str,
973 ) -> ValidationResult {
974 self.validate_value(value, &schema.0.1.value, path)
977 }
978
979 fn validate_deprecated(
980 &self,
981 value: &Value,
982 schema: &DeprecatedSchema,
983 path: &str,
984 ) -> ValidationResult {
985 let (reason, inner) = &schema.0;
986 let mut result = self.validate_value(value, &inner.value, path);
987
988 result.warning(
990 ValidationWarning::new(
991 path,
992 ValidationWarningKind::Deprecated {
993 reason: reason.clone(),
994 },
995 format!("deprecated: {}", reason),
996 )
997 .with_span(value.span),
998 );
999
1000 result
1001 }
1002
1003 fn validate_literal(&self, value: &Value, expected: &str, path: &str) -> ValidationResult {
1008 let mut result = ValidationResult::ok();
1009
1010 match value.scalar_text() {
1011 Some(text) if text == expected => {}
1012 Some(text) => {
1013 result.error(
1014 ValidationError::new(
1015 path,
1016 ValidationErrorKind::InvalidValue {
1017 reason: format!("expected literal '{expected}', got '{}'", text),
1018 },
1019 format!("expected '{expected}', got '{}'", text),
1020 )
1021 .with_span(value.span),
1022 );
1023 }
1024 None => {
1025 result.error(
1026 ValidationError::new(
1027 path,
1028 ValidationErrorKind::ExpectedScalar,
1029 format!("expected literal '{expected}', got non-scalar"),
1030 )
1031 .with_span(value.span),
1032 );
1033 }
1034 }
1035
1036 result
1037 }
1038
1039 fn validate_type_ref(
1040 &self,
1041 value: &Value,
1042 type_name: Option<&str>,
1043 path: &str,
1044 ) -> ValidationResult {
1045 let mut result = ValidationResult::ok();
1046
1047 match type_name {
1048 None => {
1049 if !value.is_unit() {
1051 result.error(
1052 ValidationError::new(
1053 path,
1054 ValidationErrorKind::TypeMismatch {
1055 expected: "unit".into(),
1056 got: value_type_name(value).into(),
1057 },
1058 "expected unit value",
1059 )
1060 .with_span(value.span),
1061 );
1062 }
1063 }
1064 Some(name) => {
1065 if let Some(type_schema) = self.schema_file.schema.get(&Some(name.to_string())) {
1067 result.merge(self.validate_value(value, type_schema, path));
1068 } else {
1069 result.error(
1070 ValidationError::new(
1071 path,
1072 ValidationErrorKind::UnknownType { name: name.into() },
1073 format!("unknown type '{name}'"),
1074 )
1075 .with_span(value.span),
1076 );
1077 }
1078 }
1079 }
1080
1081 result
1082 }
1083}
1084
1085fn value_type_name(value: &Value) -> &'static str {
1087 if value.is_unit() {
1088 return "unit";
1089 }
1090 if value.tag.is_some() {
1091 return "tagged";
1092 }
1093 match &value.payload {
1094 None => "unit",
1095 Some(Payload::Scalar(_)) => "scalar",
1096 Some(Payload::Sequence(_)) => "sequence",
1097 Some(Payload::Object(_)) => "object",
1098 }
1099}
1100
1101fn schema_type_name(schema: &Schema) -> String {
1103 match schema {
1104 Schema::String(_) => "string".into(),
1105 Schema::Int(_) => "int".into(),
1106 Schema::Float(_) => "float".into(),
1107 Schema::Bool => "bool".into(),
1108 Schema::Unit => "unit".into(),
1109 Schema::Any => "any".into(),
1110 Schema::Object(_) => "object".into(),
1111 Schema::Seq(_) => "seq".into(),
1112 Schema::Tuple(_) => "tuple".into(),
1113 Schema::Map(_) => "map".into(),
1114 Schema::Union(_) => "union".into(),
1115 Schema::Optional(_) => "optional".into(),
1116 Schema::Enum(_) => "enum".into(),
1117 Schema::OneOf(_) => "one-of".into(),
1118 Schema::Flatten(_) => "flatten".into(),
1119 Schema::Default(_) => "default".into(),
1120 Schema::Deprecated(_) => "deprecated".into(),
1121 Schema::Literal(s) => format!("literal({s})"),
1122 Schema::Type { name: None } => "unit".into(),
1123 Schema::Type { name: Some(n) } => n.clone(),
1124 }
1125}
1126
1127pub fn validate(doc: &Value, schema: &SchemaFile) -> ValidationResult {
1129 let validator = Validator::new(schema);
1130 validator.validate_document(doc)
1131}
1132
1133pub fn validate_as(value: &Value, schema: &SchemaFile, type_name: &str) -> ValidationResult {
1135 let validator = Validator::new(schema);
1136 validator.validate_as_type(value, type_name)
1137}
1138
1139#[cfg(test)]
1140mod tests {
1141 use super::*;
1142
1143 #[test]
1144 fn test_typed_catchall_validation() {
1145 let schema_source = r#"meta {id test}
1147schema {
1148 @ @object{
1149 @string @int
1150 }
1151}"#;
1152 let schema: SchemaFile = crate::from_str(schema_source).expect("should parse schema");
1153
1154 let root = schema.schema.get(&None).expect("should have root");
1156 if let Schema::Object(obj) = root {
1157 tracing::debug!("Object schema has {} entries", obj.0.len());
1158 for (key, value) in &obj.0 {
1159 tracing::debug!(
1160 "Key: value={:?}, tag={:?} -> {:?}",
1161 key.value.value,
1162 key.value.tag,
1163 value
1164 );
1165 }
1166 let catchall = obj.0.iter().find(|(k, _)| k.value.tag.is_some());
1168 assert!(
1169 catchall.is_some(),
1170 "Schema should have a typed catch-all entry. Keys: {:?}",
1171 obj.0
1172 .keys()
1173 .map(|k| (&k.value.value, &k.value.tag))
1174 .collect::<Vec<_>>()
1175 );
1176 } else {
1177 panic!("Root should be an object schema, got {:?}", root);
1178 }
1179
1180 let doc_source = r#"foo 42
1182bar 123"#;
1183 let doc = styx_tree::parse(doc_source).expect("should parse doc");
1184
1185 let result = validate(&doc, &schema);
1186 assert!(
1187 result.is_valid(),
1188 "Document should validate. Errors: {:?}",
1189 result.errors
1190 );
1191 }
1192
1193 #[test]
1194 fn test_enum_with_fallback_variant() {
1195 let schema_source = r#"meta {id test}
1197schema {
1198 @ @object{
1199 filter @enum{
1200 gt @string
1201 lt @string
1202 eq @string
1203 }
1204 }
1205}"#;
1206 let schema: SchemaFile = crate::from_str(schema_source).expect("should parse schema");
1207
1208 let doc = styx_tree::parse(r#"filter "published""#).expect("should parse doc");
1210
1211 let result = validate(&doc, &schema);
1212 assert!(
1213 result.is_valid(),
1214 "Bare string should fall back to eq variant. Errors: {:?}",
1215 result.errors
1216 );
1217 }
1218
1219 #[test]
1220 fn test_optional_accepts_unit() {
1221 let schema_source = r#"meta {id test}
1223schema {
1224 @ @object{
1225 name @optional(@string)
1226 }
1227}"#;
1228 let schema: SchemaFile = crate::from_str(schema_source).expect("should parse schema");
1229
1230 let doc = styx_tree::parse("name").expect("should parse doc");
1232
1233 let result = validate(&doc, &schema);
1234 assert!(
1235 result.is_valid(),
1236 "Unit value should be valid for @optional. Errors: {:?}",
1237 result.errors
1238 );
1239 }
1240}