1use crate::schema::graph::{GraphIndex, SchemaGraph};
19use crate::schema::metadata::TypeId;
20use crate::schema::schema_type::{
21 BinaryRestrictions, DiscriminatorRule, NumericBound, NumericRepr, NumericRestrictionError,
22 NumericRestrictions, PathSpec, QuantitySpec, QuantityValue, SchemaType, TextRestrictions,
23 UnionBranch, UrlRestrictions, numeric_bound_cmp,
24};
25#[cfg(not(all(feature = "guest", not(feature = "host"))))]
26use crate::schema::schema_value::SecretValuePayload;
27use crate::schema::schema_value::{
28 BinaryValuePayload, ResultValuePayload, SchemaValue, TextValuePayload,
29};
30#[cfg(not(all(feature = "guest", not(feature = "host"))))]
33use crate::schema::schema_type::{PermissionCardSpec, QuotaTokenSpec, SecretSpec};
34#[cfg(not(all(feature = "guest", not(feature = "host"))))]
35use crate::schema::schema_value::{PermissionCardValuePayload, QuotaTokenValuePayload};
36use std::cmp::Ordering;
37use std::error::Error;
38use std::fmt::{self, Display, Formatter, Write};
39
40#[derive(Clone, Debug, PartialEq, Eq)]
42pub enum ValuePathSegment {
43 Field(String),
44 Index(usize),
45 VariantPayload,
46 OptionInner,
47 ResultOk,
48 ResultErr,
49 UnionBody,
50 MapKey(usize),
51 MapValue(usize),
52}
53
54impl Display for ValuePathSegment {
55 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
56 match self {
57 ValuePathSegment::Field(n) => write!(f, ".field({n:?})"),
58 ValuePathSegment::Index(i) => write!(f, ".index({i})"),
59 ValuePathSegment::VariantPayload => f.write_str(".variant_payload"),
60 ValuePathSegment::OptionInner => f.write_str(".option_inner"),
61 ValuePathSegment::ResultOk => f.write_str(".ok"),
62 ValuePathSegment::ResultErr => f.write_str(".err"),
63 ValuePathSegment::UnionBody => f.write_str(".union_body"),
64 ValuePathSegment::MapKey(i) => write!(f, ".map_key({i})"),
65 ValuePathSegment::MapValue(i) => write!(f, ".map_value({i})"),
66 }
67 }
68}
69
70#[derive(Clone, Debug, Default, PartialEq, Eq)]
72pub struct ValuePath {
73 segments: Vec<ValuePathSegment>,
74}
75
76impl ValuePath {
77 pub fn new() -> Self {
78 Self::default()
79 }
80
81 pub fn segments(&self) -> &[ValuePathSegment] {
82 &self.segments
83 }
84
85 fn push(&mut self, segment: ValuePathSegment) {
86 self.segments.push(segment);
87 }
88
89 fn pop(&mut self) {
90 self.segments.pop();
91 }
92
93 fn snapshot(&self) -> Self {
94 Self {
95 segments: self.segments.clone(),
96 }
97 }
98}
99
100impl Display for ValuePath {
101 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
102 for s in &self.segments {
103 write!(f, "{s}")?;
104 }
105 Ok(())
106 }
107}
108
109#[derive(Clone, Debug, PartialEq)]
111pub enum ValueError {
112 UnsupportedFeature {
114 path: ValuePath,
115 feature: &'static str,
116 },
117 ShapeMismatch {
118 path: ValuePath,
119 expected: String,
120 found: String,
121 },
122 VariantCaseOutOfRange {
123 path: ValuePath,
124 case: u32,
125 case_count: usize,
126 },
127 EnumCaseOutOfRange {
128 path: ValuePath,
129 case: u32,
130 case_count: usize,
131 },
132 RecordArityMismatch {
133 path: ValuePath,
134 expected: usize,
135 found: usize,
136 },
137 TupleArityMismatch {
138 path: ValuePath,
139 expected: usize,
140 found: usize,
141 },
142 FlagsArityMismatch {
143 path: ValuePath,
144 expected: usize,
145 found: usize,
146 },
147 FixedListLengthMismatch {
148 path: ValuePath,
149 expected: u32,
150 found: usize,
151 },
152 DanglingRef {
153 path: ValuePath,
154 type_id: TypeId,
155 },
156 RecursiveRef {
161 path: ValuePath,
162 type_id: TypeId,
163 },
164 UnionUnknownTag {
165 path: ValuePath,
166 tag: String,
167 },
168 UnionDiscriminatorMismatch {
169 path: ValuePath,
170 tag: String,
171 },
172 VariantPayloadPresenceMismatch {
173 path: ValuePath,
174 expected_some: bool,
175 },
176 ResultPayloadPresenceMismatch {
177 path: ValuePath,
178 expected_some: bool,
179 side: ResultSide,
180 },
181 OptionInnerPresenceMismatch {
182 path: ValuePath,
183 },
184 TextLanguageNotAllowed {
185 path: ValuePath,
186 language: String,
187 },
188 TextTooShort {
189 path: ValuePath,
190 min: u32,
191 found: usize,
192 },
193 TextTooLong {
194 path: ValuePath,
195 max: u32,
196 found: usize,
197 },
198 TextRegexMismatch {
199 path: ValuePath,
200 regex: String,
201 },
202 BinaryMimeNotAllowed {
203 path: ValuePath,
204 mime_type: String,
205 },
206 BinaryTooSmall {
207 path: ValuePath,
208 min: u32,
209 found: usize,
210 },
211 BinaryTooLarge {
212 path: ValuePath,
213 max: u32,
214 found: usize,
215 },
216 PathEmpty {
217 path: ValuePath,
218 },
219 PathExtensionNotAllowed {
220 path: ValuePath,
221 extension: String,
222 },
223 UrlEmpty {
224 path: ValuePath,
225 },
226 UrlInvalid {
227 path: ValuePath,
228 url: String,
229 reason: String,
230 },
231 UrlSchemeNotAllowed {
232 path: ValuePath,
233 scheme: String,
234 },
235 UrlHostNotAllowed {
236 path: ValuePath,
237 host: String,
238 },
239 UrlHostMissing {
240 path: ValuePath,
241 },
242 QuantityUnitNotAllowed {
243 path: ValuePath,
244 unit: String,
245 },
246 QuantityOutOfRange {
247 path: ValuePath,
248 reason: String,
249 },
250 NumericOutOfRange {
251 path: ValuePath,
252 reason: String,
253 },
254 SecretCategoryMismatch {
255 path: ValuePath,
256 expected: String,
257 found: Option<String>,
258 },
259 QuotaTokenResourceMismatch {
260 path: ValuePath,
261 expected: String,
262 found: String,
263 },
264 PermissionCardPolymorphicMismatch {
265 path: ValuePath,
266 expected: bool,
267 found: bool,
268 },
269}
270
271#[derive(Copy, Clone, Debug, PartialEq, Eq)]
272pub enum ResultSide {
273 Ok,
274 Err,
275}
276
277impl Display for ValueError {
278 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
279 match self {
280 ValueError::UnsupportedFeature { path, feature } => {
281 write!(f, "validation at {path} requires feature `{feature}`")
282 }
283 ValueError::ShapeMismatch {
284 path,
285 expected,
286 found,
287 } => write!(
288 f,
289 "shape mismatch at {path}: expected {expected}, found {found}"
290 ),
291 ValueError::VariantCaseOutOfRange {
292 path,
293 case,
294 case_count,
295 } => write!(
296 f,
297 "variant case index {case} at {path} is out of range (case count: {case_count})"
298 ),
299 ValueError::EnumCaseOutOfRange {
300 path,
301 case,
302 case_count,
303 } => write!(
304 f,
305 "enum case index {case} at {path} is out of range (case count: {case_count})"
306 ),
307 ValueError::RecordArityMismatch {
308 path,
309 expected,
310 found,
311 } => write!(
312 f,
313 "record at {path} has {found} field(s), expected {expected}"
314 ),
315 ValueError::TupleArityMismatch {
316 path,
317 expected,
318 found,
319 } => write!(
320 f,
321 "tuple at {path} has {found} element(s), expected {expected}"
322 ),
323 ValueError::FlagsArityMismatch {
324 path,
325 expected,
326 found,
327 } => write!(
328 f,
329 "flags value at {path} has {found} bit(s), expected {expected}"
330 ),
331 ValueError::FixedListLengthMismatch {
332 path,
333 expected,
334 found,
335 } => write!(
336 f,
337 "fixed-list at {path} has {found} element(s), expected {expected}"
338 ),
339 ValueError::DanglingRef { path, type_id } => write!(
340 f,
341 "dangling ref `{type_id}` at {path} (no such named definition)"
342 ),
343 ValueError::RecursiveRef { path, type_id } => write!(
344 f,
345 "ref chain at {path} loops back to `{type_id}` without reaching a structural shape"
346 ),
347 ValueError::UnionUnknownTag { path, tag } => write!(
348 f,
349 "union value at {path} carries tag `{tag}` that does not match any branch"
350 ),
351 ValueError::UnionDiscriminatorMismatch { path, tag } => write!(
352 f,
353 "union value at {path} does not satisfy branch `{tag}` discriminator"
354 ),
355 ValueError::VariantPayloadPresenceMismatch {
356 path,
357 expected_some,
358 } => {
359 let mut s = String::new();
360 let _ = write!(s, "variant payload presence mismatch at {path}: ");
361 if *expected_some {
362 let _ = write!(s, "schema expects a payload, value carries none");
363 } else {
364 let _ = write!(s, "schema expects no payload, value carries one");
365 }
366 f.write_str(&s)
367 }
368 ValueError::ResultPayloadPresenceMismatch {
369 path,
370 expected_some,
371 side,
372 } => {
373 let side = match side {
374 ResultSide::Ok => "ok",
375 ResultSide::Err => "err",
376 };
377 let mut s = String::new();
378 let _ = write!(s, "result {side} payload presence mismatch at {path}: ");
379 if *expected_some {
380 let _ = write!(s, "schema expects a payload, value carries none");
381 } else {
382 let _ = write!(s, "schema expects no payload, value carries one");
383 }
384 f.write_str(&s)
385 }
386 ValueError::OptionInnerPresenceMismatch { path } => {
387 write!(f, "option-value presence inconsistent at {path}")
388 }
389 ValueError::TextLanguageNotAllowed { path, language } => write!(
390 f,
391 "text value at {path} carries language `{language}` not in the allow-list"
392 ),
393 ValueError::TextTooShort { path, min, found } => write!(
394 f,
395 "text value at {path} has {found} char(s), below min-length {min}"
396 ),
397 ValueError::TextTooLong { path, max, found } => write!(
398 f,
399 "text value at {path} has {found} char(s), above max-length {max}"
400 ),
401 ValueError::TextRegexMismatch { path, regex } => write!(
402 f,
403 "text value at {path} does not match required regex `{regex}`"
404 ),
405 ValueError::BinaryMimeNotAllowed { path, mime_type } => write!(
406 f,
407 "binary value at {path} carries mime-type `{mime_type}` not in the allow-list"
408 ),
409 ValueError::BinaryTooSmall { path, min, found } => write!(
410 f,
411 "binary value at {path} has {found} byte(s), below min-bytes {min}"
412 ),
413 ValueError::BinaryTooLarge { path, max, found } => write!(
414 f,
415 "binary value at {path} has {found} byte(s), above max-bytes {max}"
416 ),
417 ValueError::PathEmpty { path } => write!(f, "path value at {path} is empty"),
418 ValueError::PathExtensionNotAllowed { path, extension } => write!(
419 f,
420 "path value at {path} has extension `{extension}` not in the allow-list"
421 ),
422 ValueError::UrlEmpty { path } => write!(f, "url value at {path} is empty"),
423 ValueError::UrlInvalid { path, url, reason } => write!(
424 f,
425 "url value at {path} (`{url}`) is not a valid URL: {reason}"
426 ),
427 ValueError::UrlSchemeNotAllowed { path, scheme } => write!(
428 f,
429 "url value at {path} has scheme `{scheme}` not in the allow-list"
430 ),
431 ValueError::UrlHostNotAllowed { path, host } => write!(
432 f,
433 "url value at {path} has host `{host}` not in the allow-list"
434 ),
435 ValueError::UrlHostMissing { path } => write!(
436 f,
437 "url value at {path} has no host (allow-list requires one)"
438 ),
439 ValueError::QuantityUnitNotAllowed { path, unit } => write!(
440 f,
441 "quantity value at {path} has unit `{unit}` which is not allowed"
442 ),
443 ValueError::QuantityOutOfRange { path, reason } => {
444 write!(f, "quantity value at {path} is out of range ({reason})")
445 }
446 ValueError::NumericOutOfRange { path, reason } => {
447 write!(f, "numeric value at {path} is out of range ({reason})")
448 }
449 ValueError::SecretCategoryMismatch {
450 path,
451 expected,
452 found,
453 } => write!(
454 f,
455 "secret value at {path} expected category `{expected}`, found `{}`",
456 found.as_deref().unwrap_or("<none>")
457 ),
458 ValueError::QuotaTokenResourceMismatch {
459 path,
460 expected,
461 found,
462 } => write!(
463 f,
464 "quota-token value at {path} expected resource `{expected}`, found `{found}`"
465 ),
466 ValueError::PermissionCardPolymorphicMismatch {
467 path,
468 expected,
469 found,
470 } => write!(
471 f,
472 "permission-card value at {path} expected polymorphic `{expected}`, found `{found}`"
473 ),
474 }
475 }
476}
477
478impl Error for ValueError {}
479
480struct ValidationContext<'a> {
481 index: GraphIndex<'a>,
482 #[cfg(feature = "regex")]
483 regexes: std::collections::HashMap<&'a str, Option<regex::Regex>>,
484}
485
486impl<'a> ValidationContext<'a> {
487 fn new(graph: &'a SchemaGraph) -> Self {
488 Self {
489 index: GraphIndex::new(graph),
490 #[cfg(feature = "regex")]
491 regexes: std::collections::HashMap::new(),
492 }
493 }
494
495 fn prepare(&mut self, ty: &'a SchemaType) {
496 #[cfg(feature = "regex")]
497 self.prepare_regexes(ty, &mut std::collections::HashSet::new());
498 #[cfg(not(feature = "regex"))]
499 let _ = ty;
500 }
501
502 #[cfg(feature = "regex")]
503 fn prepare_regexes(
504 &mut self,
505 mut ty: &'a SchemaType,
506 visited: &mut std::collections::HashSet<&'a TypeId>,
507 ) {
508 while let SchemaType::Ref { id, .. } = ty {
509 if !visited.insert(id) {
510 return;
511 }
512 let Some(def) = self.index.lookup(id) else {
513 return;
514 };
515 ty = &def.body;
516 }
517 match ty {
518 SchemaType::Record { fields, .. } => {
519 for field in fields {
520 self.prepare_regexes(&field.body, visited);
521 }
522 }
523 SchemaType::Variant { cases, .. } => {
524 for case in cases {
525 if let Some(ty) = &case.payload {
526 self.prepare_regexes(ty, visited);
527 }
528 }
529 }
530 SchemaType::Tuple { elements, .. } => {
531 for ty in elements {
532 self.prepare_regexes(ty, visited);
533 }
534 }
535 SchemaType::List { element, .. } | SchemaType::FixedList { element, .. } => {
536 self.prepare_regexes(element, visited)
537 }
538 SchemaType::Map { key, value, .. } => {
539 self.prepare_regexes(key, visited);
540 self.prepare_regexes(value, visited);
541 }
542 SchemaType::Option { inner, .. } => self.prepare_regexes(inner, visited),
543 SchemaType::Result { spec, .. } => {
544 for ty in [spec.ok.as_deref(), spec.err.as_deref()]
545 .into_iter()
546 .flatten()
547 {
548 self.prepare_regexes(ty, visited);
549 }
550 }
551 SchemaType::Text { restrictions, .. } => {
552 if let Some(pattern) = &restrictions.regex {
553 self.prepare_regex(pattern);
554 }
555 }
556 SchemaType::Union { spec, .. } => {
557 for branch in &spec.branches {
558 self.prepare_regexes(&branch.body, visited);
559 if let DiscriminatorRule::Regex { regex } = &branch.discriminator {
560 self.prepare_regex(regex);
561 }
562 }
563 }
564 _ => {}
565 }
566 }
567
568 #[cfg(feature = "regex")]
569 fn prepare_regex(&mut self, pattern: &'a str) {
570 self.regexes
571 .entry(pattern)
572 .or_insert_with(|| regex::Regex::new(pattern).ok());
573 }
574}
575
576impl<'a> std::ops::Deref for ValidationContext<'a> {
577 type Target = GraphIndex<'a>;
578 fn deref(&self) -> &Self::Target {
579 &self.index
580 }
581}
582
583pub struct PreparedValueValidator<'a> {
586 context: ValidationContext<'a>,
587 ty: &'a SchemaType,
588}
589
590impl<'a> PreparedValueValidator<'a> {
591 pub fn new(graph: &'a SchemaGraph, ty: &'a SchemaType) -> Self {
592 let mut context = ValidationContext::new(graph);
593 context.prepare(ty);
594 Self { context, ty }
595 }
596
597 pub fn validate(&self, value: &SchemaValue) -> Result<(), Vec<ValueError>> {
598 let mut errors = Vec::new();
599 let mut path = ValuePath::new();
600 check(&self.context, self.ty, value, &mut path, &mut errors);
601 if errors.is_empty() {
602 Ok(())
603 } else {
604 Err(errors)
605 }
606 }
607}
608
609pub fn validate_value(
612 graph: &SchemaGraph,
613 ty: &SchemaType,
614 value: &SchemaValue,
615) -> Result<(), Vec<ValueError>> {
616 PreparedValueValidator::new(graph, ty).validate(value)
617}
618
619pub fn validate_record_fields<'g, 'f, I>(
634 graph: &'g SchemaGraph,
635 fields: I,
636 values: &[SchemaValue],
637) -> Result<(), Vec<ValueError>>
638where
639 I: Iterator<Item = (&'f str, &'f SchemaType)> + Clone,
640{
641 let mut errors = Vec::new();
642 let mut path = ValuePath::new();
643 let mut index = ValidationContext::new(graph);
644
645 let field_count = fields.clone().count();
646 if field_count != values.len() {
647 errors.push(ValueError::RecordArityMismatch {
648 path: path.snapshot(),
649 expected: field_count,
650 found: values.len(),
651 });
652 return Err(errors);
653 }
654
655 for (_, body) in fields.clone() {
656 index.prepare(body);
657 }
658 for ((name, body), v) in fields.zip(values.iter()) {
659 path.push(ValuePathSegment::Field(name.to_string()));
660 check(&index, body, v, &mut path, &mut errors);
661 path.pop();
662 }
663
664 if errors.is_empty() {
665 Ok(())
666 } else {
667 Err(errors)
668 }
669}
670
671fn shape_name(value: &SchemaValue) -> &'static str {
672 match value {
673 SchemaValue::Bool(_) => "bool",
674 SchemaValue::S8(_) => "s8",
675 SchemaValue::S16(_) => "s16",
676 SchemaValue::S32(_) => "s32",
677 SchemaValue::S64(_) => "s64",
678 SchemaValue::U8(_) => "u8",
679 SchemaValue::U16(_) => "u16",
680 SchemaValue::U32(_) => "u32",
681 SchemaValue::U64(_) => "u64",
682 SchemaValue::F32(_) => "f32",
683 SchemaValue::F64(_) => "f64",
684 SchemaValue::Char(_) => "char",
685 SchemaValue::String(_) => "string",
686 SchemaValue::Record { .. } => "record",
687 SchemaValue::Variant(_) => "variant",
688 SchemaValue::Enum { .. } => "enum",
689 SchemaValue::Flags { .. } => "flags",
690 SchemaValue::Tuple { .. } => "tuple",
691 SchemaValue::List { .. } => "list",
692 SchemaValue::FixedList { .. } => "fixed-list",
693 SchemaValue::Map { .. } => "map",
694 SchemaValue::Option { .. } => "option",
695 SchemaValue::Result(_) => "result",
696 SchemaValue::Text(_) => "text",
697 SchemaValue::Binary(_) => "binary",
698 SchemaValue::Path { .. } => "path",
699 SchemaValue::Url { .. } => "url",
700 SchemaValue::Uuid(_) => "uuid",
701 SchemaValue::Datetime { .. } => "datetime",
702 SchemaValue::Duration(_) => "duration",
703 SchemaValue::Quantity(_) => "quantity",
704 SchemaValue::Union(_) => "union",
705 SchemaValue::Secret(_) => "secret",
706 SchemaValue::QuotaToken(_) => "quota-token",
707 SchemaValue::Stream(_) => "stream",
708 SchemaValue::PermissionCard(_) => "permission-card",
709 }
710}
711
712fn type_name(ty: &SchemaType) -> &'static str {
713 match ty {
714 SchemaType::Ref { .. } => "ref",
715 SchemaType::Bool { .. } => "bool",
716 SchemaType::S8 { .. } => "s8",
717 SchemaType::S16 { .. } => "s16",
718 SchemaType::S32 { .. } => "s32",
719 SchemaType::S64 { .. } => "s64",
720 SchemaType::U8 { .. } => "u8",
721 SchemaType::U16 { .. } => "u16",
722 SchemaType::U32 { .. } => "u32",
723 SchemaType::U64 { .. } => "u64",
724 SchemaType::F32 { .. } => "f32",
725 SchemaType::F64 { .. } => "f64",
726 SchemaType::Char { .. } => "char",
727 SchemaType::String { .. } => "string",
728 SchemaType::Record { .. } => "record",
729 SchemaType::Variant { .. } => "variant",
730 SchemaType::Enum { .. } => "enum",
731 SchemaType::Flags { .. } => "flags",
732 SchemaType::Tuple { .. } => "tuple",
733 SchemaType::List { .. } => "list",
734 SchemaType::FixedList { .. } => "fixed-list",
735 SchemaType::Map { .. } => "map",
736 SchemaType::Option { .. } => "option",
737 SchemaType::Result { .. } => "result",
738 SchemaType::Text { .. } => "text",
739 SchemaType::Binary { .. } => "binary",
740 SchemaType::Path { .. } => "path",
741 SchemaType::Url { .. } => "url",
742 SchemaType::Uuid { .. } => "uuid",
743 SchemaType::Datetime { .. } => "datetime",
744 SchemaType::Duration { .. } => "duration",
745 SchemaType::Quantity { .. } => "quantity",
746 SchemaType::Union { .. } => "union",
747 SchemaType::Secret { .. } => "secret",
748 SchemaType::QuotaToken { .. } => "quota-token",
749 SchemaType::PermissionCard { .. } => "permission-card",
750 SchemaType::Future { .. } => "future",
751 SchemaType::Stream { .. } => "stream",
752 }
753}
754
755fn shape_mismatch(
756 path: &ValuePath,
757 ty: &SchemaType,
758 value: &SchemaValue,
759 errors: &mut Vec<ValueError>,
760) {
761 errors.push(ValueError::ShapeMismatch {
762 path: path.snapshot(),
763 expected: type_name(ty).to_string(),
764 found: shape_name(value).to_string(),
765 });
766}
767
768fn resolve_refs_at_value_node<'a>(
780 index: &GraphIndex<'a>,
781 mut ty: &'a SchemaType,
782 path: &ValuePath,
783 errors: &mut Vec<ValueError>,
784) -> Option<&'a SchemaType> {
785 let max_hops = index.defs_len();
791 let mut hops = 0usize;
792 loop {
793 match ty {
794 SchemaType::Ref { id, .. } => {
795 if hops > max_hops {
796 errors.push(ValueError::RecursiveRef {
797 path: path.snapshot(),
798 type_id: id.clone(),
799 });
800 return None;
801 }
802 hops += 1;
803 match index.lookup(id) {
804 Some(def) => {
805 ty = &def.body;
806 }
807 None => {
808 errors.push(ValueError::DanglingRef {
809 path: path.snapshot(),
810 type_id: id.clone(),
811 });
812 return None;
813 }
814 }
815 }
816 other => return Some(other),
817 }
818 }
819}
820
821fn check<'a>(
822 index: &ValidationContext<'a>,
823 ty: &'a SchemaType,
824 value: &SchemaValue,
825 path: &mut ValuePath,
826 errors: &mut Vec<ValueError>,
827) {
828 let Some(ty) = resolve_refs_at_value_node(index, ty, path, errors) else {
834 return;
835 };
836
837 match (ty, value) {
838 (SchemaType::Bool { .. }, SchemaValue::Bool(_)) => {}
839 (SchemaType::S8 { restrictions, .. }, SchemaValue::S8(v)) => {
840 check_numeric(
841 NumericRepr::S8,
842 restrictions,
843 NumericBound::Signed(*v as i64),
844 path,
845 errors,
846 );
847 }
848 (SchemaType::S16 { restrictions, .. }, SchemaValue::S16(v)) => {
849 check_numeric(
850 NumericRepr::S16,
851 restrictions,
852 NumericBound::Signed(*v as i64),
853 path,
854 errors,
855 );
856 }
857 (SchemaType::S32 { restrictions, .. }, SchemaValue::S32(v)) => {
858 check_numeric(
859 NumericRepr::S32,
860 restrictions,
861 NumericBound::Signed(*v as i64),
862 path,
863 errors,
864 );
865 }
866 (SchemaType::S64 { restrictions, .. }, SchemaValue::S64(v)) => {
867 check_numeric(
868 NumericRepr::S64,
869 restrictions,
870 NumericBound::Signed(*v),
871 path,
872 errors,
873 );
874 }
875 (SchemaType::U8 { restrictions, .. }, SchemaValue::U8(v)) => {
876 check_numeric(
877 NumericRepr::U8,
878 restrictions,
879 NumericBound::Unsigned(*v as u64),
880 path,
881 errors,
882 );
883 }
884 (SchemaType::U16 { restrictions, .. }, SchemaValue::U16(v)) => {
885 check_numeric(
886 NumericRepr::U16,
887 restrictions,
888 NumericBound::Unsigned(*v as u64),
889 path,
890 errors,
891 );
892 }
893 (SchemaType::U32 { restrictions, .. }, SchemaValue::U32(v)) => {
894 check_numeric(
895 NumericRepr::U32,
896 restrictions,
897 NumericBound::Unsigned(*v as u64),
898 path,
899 errors,
900 );
901 }
902 (SchemaType::U64 { restrictions, .. }, SchemaValue::U64(v)) => {
903 check_numeric(
904 NumericRepr::U64,
905 restrictions,
906 NumericBound::Unsigned(*v),
907 path,
908 errors,
909 );
910 }
911 (SchemaType::F32 { restrictions, .. }, SchemaValue::F32(v)) => {
912 check_numeric(
913 NumericRepr::F32,
914 restrictions,
915 NumericBound::FloatBits((*v as f64).to_bits()),
916 path,
917 errors,
918 );
919 }
920 (SchemaType::F64 { restrictions, .. }, SchemaValue::F64(v)) => {
921 check_numeric(
922 NumericRepr::F64,
923 restrictions,
924 NumericBound::FloatBits(v.to_bits()),
925 path,
926 errors,
927 );
928 }
929 (SchemaType::Char { .. }, SchemaValue::Char(_)) => {}
930 (SchemaType::String { .. }, SchemaValue::String(_)) => {}
931
932 (SchemaType::Text { restrictions, .. }, SchemaValue::Text(payload)) => {
933 check_text(index, restrictions, payload, path, errors);
934 }
935 (SchemaType::Binary { restrictions, .. }, SchemaValue::Binary(payload)) => {
936 check_binary(restrictions, payload, path, errors);
937 }
938 (SchemaType::Path { spec, .. }, SchemaValue::Path { path: p }) => {
939 check_path(spec, p.as_str(), path, errors);
940 }
941 (SchemaType::Url { restrictions, .. }, SchemaValue::Url { url }) => {
942 check_url(restrictions, url.as_str(), path, errors);
943 }
944 (SchemaType::Uuid { .. }, SchemaValue::Uuid(_)) => {}
945 (SchemaType::Datetime { .. }, SchemaValue::Datetime { .. }) => {}
946 (SchemaType::Duration { .. }, SchemaValue::Duration(_)) => {}
947 (SchemaType::Quantity { spec, .. }, SchemaValue::Quantity(value)) => {
948 check_quantity(spec, value, path, errors);
949 }
950 (SchemaType::Secret { spec, .. }, SchemaValue::Secret(payload)) => {
951 #[cfg(not(all(feature = "guest", not(feature = "host"))))]
952 check_secret(spec, payload, path, errors);
953 #[cfg(all(feature = "guest", not(feature = "host")))]
957 let _ = (spec, payload);
958 }
959 (SchemaType::QuotaToken { spec, .. }, SchemaValue::QuotaToken(payload)) => {
960 #[cfg(not(all(feature = "guest", not(feature = "host"))))]
961 check_quota_token(spec, payload, path, errors);
962 #[cfg(all(feature = "guest", not(feature = "host")))]
966 let _ = (spec, payload);
967 }
968 (SchemaType::Stream { .. }, SchemaValue::Stream(_)) => {}
969 (SchemaType::PermissionCard { spec, .. }, SchemaValue::PermissionCard(payload)) => {
970 #[cfg(not(all(feature = "guest", not(feature = "host"))))]
971 check_permission_card(spec, payload, path, errors);
972 #[cfg(all(feature = "guest", not(feature = "host")))]
976 let _ = (spec, payload);
977 }
978
979 (SchemaType::Record { fields, .. }, SchemaValue::Record { fields: vs }) => {
980 if fields.len() != vs.len() {
981 errors.push(ValueError::RecordArityMismatch {
982 path: path.snapshot(),
983 expected: fields.len(),
984 found: vs.len(),
985 });
986 return;
987 }
988 for (field, v) in fields.iter().zip(vs.iter()) {
989 path.push(ValuePathSegment::Field(field.name.clone()));
990 check(index, &field.body, v, path, errors);
991 path.pop();
992 }
993 }
994
995 (SchemaType::Variant { cases, .. }, SchemaValue::Variant(vp)) => {
996 let idx = vp.case as usize;
997 if idx >= cases.len() {
998 errors.push(ValueError::VariantCaseOutOfRange {
999 path: path.snapshot(),
1000 case: vp.case,
1001 case_count: cases.len(),
1002 });
1003 return;
1004 }
1005 let case = &cases[idx];
1006 match (&case.payload, &vp.payload) {
1007 (Some(case_ty), Some(payload)) => {
1008 path.push(ValuePathSegment::VariantPayload);
1009 check(index, case_ty, payload, path, errors);
1010 path.pop();
1011 }
1012 (None, None) => {}
1013 (Some(_), None) => errors.push(ValueError::VariantPayloadPresenceMismatch {
1014 path: path.snapshot(),
1015 expected_some: true,
1016 }),
1017 (None, Some(_)) => errors.push(ValueError::VariantPayloadPresenceMismatch {
1018 path: path.snapshot(),
1019 expected_some: false,
1020 }),
1021 }
1022 }
1023
1024 (SchemaType::Enum { cases, .. }, SchemaValue::Enum { case }) => {
1025 if (*case as usize) >= cases.len() {
1026 errors.push(ValueError::EnumCaseOutOfRange {
1027 path: path.snapshot(),
1028 case: *case,
1029 case_count: cases.len(),
1030 });
1031 }
1032 }
1033
1034 (SchemaType::Flags { flags, .. }, SchemaValue::Flags { bits }) => {
1035 if flags.len() != bits.len() {
1036 errors.push(ValueError::FlagsArityMismatch {
1037 path: path.snapshot(),
1038 expected: flags.len(),
1039 found: bits.len(),
1040 });
1041 }
1042 }
1043
1044 (SchemaType::Tuple { elements, .. }, SchemaValue::Tuple { elements: vs }) => {
1045 if elements.len() != vs.len() {
1046 errors.push(ValueError::TupleArityMismatch {
1047 path: path.snapshot(),
1048 expected: elements.len(),
1049 found: vs.len(),
1050 });
1051 return;
1052 }
1053 for (i, (t, v)) in elements.iter().zip(vs.iter()).enumerate() {
1054 path.push(ValuePathSegment::Index(i));
1055 check(index, t, v, path, errors);
1056 path.pop();
1057 }
1058 }
1059
1060 (SchemaType::List { element, .. }, SchemaValue::List { elements }) => {
1061 for (i, v) in elements.iter().enumerate() {
1062 path.push(ValuePathSegment::Index(i));
1063 check(index, element, v, path, errors);
1064 path.pop();
1065 }
1066 }
1067
1068 (
1069 SchemaType::FixedList {
1070 element, length, ..
1071 },
1072 SchemaValue::FixedList { elements },
1073 ) => {
1074 if elements.len() != *length as usize {
1075 errors.push(ValueError::FixedListLengthMismatch {
1076 path: path.snapshot(),
1077 expected: *length,
1078 found: elements.len(),
1079 });
1080 return;
1081 }
1082 for (i, v) in elements.iter().enumerate() {
1083 path.push(ValuePathSegment::Index(i));
1084 check(index, element, v, path, errors);
1085 path.pop();
1086 }
1087 }
1088
1089 (
1090 SchemaType::Map {
1091 key, value: vty, ..
1092 },
1093 SchemaValue::Map { entries },
1094 ) => {
1095 for (i, (k, v)) in entries.iter().enumerate() {
1096 path.push(ValuePathSegment::MapKey(i));
1097 check(index, key, k, path, errors);
1098 path.pop();
1099 path.push(ValuePathSegment::MapValue(i));
1100 check(index, vty, v, path, errors);
1101 path.pop();
1102 }
1103 }
1104
1105 (SchemaType::Option { inner, .. }, SchemaValue::Option { inner: v }) => {
1106 if let Some(v) = v {
1107 path.push(ValuePathSegment::OptionInner);
1108 check(index, inner, v, path, errors);
1109 path.pop();
1110 }
1111 }
1112
1113 (SchemaType::Result { spec, .. }, SchemaValue::Result(vp)) => match vp {
1114 ResultValuePayload::Ok { value: v } => match (&spec.ok, v) {
1115 (Some(t), Some(v)) => {
1116 path.push(ValuePathSegment::ResultOk);
1117 check(index, t, v, path, errors);
1118 path.pop();
1119 }
1120 (None, None) => {}
1121 (Some(_), None) => errors.push(ValueError::ResultPayloadPresenceMismatch {
1122 path: path.snapshot(),
1123 expected_some: true,
1124 side: ResultSide::Ok,
1125 }),
1126 (None, Some(_)) => errors.push(ValueError::ResultPayloadPresenceMismatch {
1127 path: path.snapshot(),
1128 expected_some: false,
1129 side: ResultSide::Ok,
1130 }),
1131 },
1132 ResultValuePayload::Err { value: v } => match (&spec.err, v) {
1133 (Some(t), Some(v)) => {
1134 path.push(ValuePathSegment::ResultErr);
1135 check(index, t, v, path, errors);
1136 path.pop();
1137 }
1138 (None, None) => {}
1139 (Some(_), None) => errors.push(ValueError::ResultPayloadPresenceMismatch {
1140 path: path.snapshot(),
1141 expected_some: true,
1142 side: ResultSide::Err,
1143 }),
1144 (None, Some(_)) => errors.push(ValueError::ResultPayloadPresenceMismatch {
1145 path: path.snapshot(),
1146 expected_some: false,
1147 side: ResultSide::Err,
1148 }),
1149 },
1150 },
1151
1152 (SchemaType::Union { spec, .. }, SchemaValue::Union(vp)) => {
1153 let branch = spec.branches.iter().find(|b| b.tag == vp.tag);
1154 match branch {
1155 None => errors.push(ValueError::UnionUnknownTag {
1156 path: path.snapshot(),
1157 tag: vp.tag.clone(),
1158 }),
1159 Some(branch) => {
1160 path.push(ValuePathSegment::UnionBody);
1161 #[cfg(not(feature = "regex"))]
1162 if matches!(branch.discriminator, DiscriminatorRule::Regex { .. }) {
1163 errors.push(ValueError::UnsupportedFeature {
1164 path: path.snapshot(),
1165 feature: "regex",
1166 });
1167 path.pop();
1168 return;
1169 }
1170 let mut sub_errors = Vec::new();
1171 check(index, &branch.body, &vp.body, path, &mut sub_errors);
1172 errors.extend(sub_errors);
1173 if !discriminator_matches(index, branch, &vp.body) {
1174 errors.push(ValueError::UnionDiscriminatorMismatch {
1175 path: path.snapshot(),
1176 tag: vp.tag.clone(),
1177 });
1178 }
1179 path.pop();
1180 }
1181 }
1182 }
1183
1184 (ty, v) => shape_mismatch(path, ty, v, errors),
1185 }
1186}
1187
1188fn check_numeric(
1197 repr: NumericRepr,
1198 restrictions: &Option<NumericRestrictions>,
1199 value: NumericBound,
1200 path: &mut ValuePath,
1201 errors: &mut Vec<ValueError>,
1202) {
1203 let Some(restrictions) = restrictions else {
1204 return;
1205 };
1206 if let Err(err) = restrictions.validate_for_repr(repr)
1212 && !matches!(err, NumericRestrictionError::MinGreaterThanMax)
1213 {
1214 return;
1215 }
1216 if let Some(min) = restrictions.min
1217 && !matches!(
1218 numeric_bound_cmp(value, min),
1219 Some(Ordering::Greater | Ordering::Equal)
1220 )
1221 {
1222 errors.push(ValueError::NumericOutOfRange {
1223 path: path.snapshot(),
1224 reason: format!("below minimum {}", describe_numeric_bound(min)),
1225 });
1226 }
1227 if let Some(max) = restrictions.max
1228 && !matches!(
1229 numeric_bound_cmp(value, max),
1230 Some(Ordering::Less | Ordering::Equal)
1231 )
1232 {
1233 errors.push(ValueError::NumericOutOfRange {
1234 path: path.snapshot(),
1235 reason: format!("above maximum {}", describe_numeric_bound(max)),
1236 });
1237 }
1238}
1239
1240fn describe_numeric_bound(bound: NumericBound) -> String {
1241 match bound {
1242 NumericBound::Signed(v) => v.to_string(),
1243 NumericBound::Unsigned(v) => v.to_string(),
1244 NumericBound::FloatBits(bits) => f64::from_bits(bits).to_string(),
1245 }
1246}
1247
1248fn check_text(
1249 _context: &ValidationContext,
1250 restrictions: &TextRestrictions,
1251 payload: &TextValuePayload,
1252 path: &mut ValuePath,
1253 errors: &mut Vec<ValueError>,
1254) {
1255 if let (Some(allowed), Some(lang)) = (&restrictions.languages, &payload.language)
1256 && !allowed.iter().any(|a| a == lang)
1257 {
1258 errors.push(ValueError::TextLanguageNotAllowed {
1259 path: path.snapshot(),
1260 language: lang.clone(),
1261 });
1262 }
1263 let char_len = payload.text.chars().count();
1264 if let Some(min) = restrictions.min_length
1265 && (char_len as u64) < (min as u64)
1266 {
1267 errors.push(ValueError::TextTooShort {
1268 path: path.snapshot(),
1269 min,
1270 found: char_len,
1271 });
1272 }
1273 if let Some(max) = restrictions.max_length
1274 && (char_len as u64) > (max as u64)
1275 {
1276 errors.push(ValueError::TextTooLong {
1277 path: path.snapshot(),
1278 max,
1279 found: char_len,
1280 });
1281 }
1282 #[cfg(not(feature = "regex"))]
1283 if restrictions.regex.is_some() {
1284 errors.push(ValueError::UnsupportedFeature {
1285 path: path.snapshot(),
1286 feature: "regex",
1287 });
1288 }
1289 #[cfg(feature = "regex")]
1290 if let Some(regex) = &restrictions.regex
1291 && let Some(Some(compiled)) = _context.regexes.get(regex.as_str())
1292 && !compiled.is_match(payload.text.as_str())
1293 {
1294 errors.push(ValueError::TextRegexMismatch {
1295 path: path.snapshot(),
1296 regex: regex.clone(),
1297 });
1298 }
1299}
1300
1301fn check_binary(
1302 restrictions: &BinaryRestrictions,
1303 payload: &BinaryValuePayload,
1304 path: &mut ValuePath,
1305 errors: &mut Vec<ValueError>,
1306) {
1307 if let (Some(allowed), Some(mime)) = (&restrictions.mime_types, &payload.mime_type)
1308 && !allowed.iter().any(|a| a == mime)
1309 {
1310 errors.push(ValueError::BinaryMimeNotAllowed {
1311 path: path.snapshot(),
1312 mime_type: mime.clone(),
1313 });
1314 }
1315 let len = payload.bytes.len();
1316 if let Some(min) = restrictions.min_bytes
1317 && (len as u64) < (min as u64)
1318 {
1319 errors.push(ValueError::BinaryTooSmall {
1320 path: path.snapshot(),
1321 min,
1322 found: len,
1323 });
1324 }
1325 if let Some(max) = restrictions.max_bytes
1326 && (len as u64) > (max as u64)
1327 {
1328 errors.push(ValueError::BinaryTooLarge {
1329 path: path.snapshot(),
1330 max,
1331 found: len,
1332 });
1333 }
1334}
1335
1336fn check_path(spec: &PathSpec, p: &str, path: &mut ValuePath, errors: &mut Vec<ValueError>) {
1337 if p.is_empty() {
1338 errors.push(ValueError::PathEmpty {
1339 path: path.snapshot(),
1340 });
1341 return;
1342 }
1343 if let Some(allowed_exts) = &spec.allowed_extensions
1344 && let Some(ext) = file_extension(p)
1345 && !allowed_exts.iter().any(|a| a == ext)
1346 {
1347 errors.push(ValueError::PathExtensionNotAllowed {
1348 path: path.snapshot(),
1349 extension: ext.to_string(),
1350 });
1351 }
1352 }
1359
1360#[cfg(not(feature = "url"))]
1361fn check_url(
1362 _spec: &UrlRestrictions,
1363 _url: &str,
1364 path: &mut ValuePath,
1365 errors: &mut Vec<ValueError>,
1366) {
1367 errors.push(ValueError::UnsupportedFeature {
1368 path: path.snapshot(),
1369 feature: "url",
1370 });
1371}
1372
1373#[cfg(feature = "url")]
1374fn check_url(
1375 spec: &UrlRestrictions,
1376 url: &str,
1377 path: &mut ValuePath,
1378 errors: &mut Vec<ValueError>,
1379) {
1380 if url.is_empty() {
1381 errors.push(ValueError::UrlEmpty {
1382 path: path.snapshot(),
1383 });
1384 return;
1385 }
1386 let parsed = match url::Url::parse(url) {
1387 Ok(parsed) => parsed,
1388 Err(err) => {
1389 errors.push(ValueError::UrlInvalid {
1390 path: path.snapshot(),
1391 url: url.to_string(),
1392 reason: err.to_string(),
1393 });
1394 return;
1395 }
1396 };
1397 if let Some(allowed) = &spec.allowed_schemes
1398 && !allowed
1399 .iter()
1400 .any(|scheme| scheme.eq_ignore_ascii_case(parsed.scheme()))
1401 {
1402 errors.push(ValueError::UrlSchemeNotAllowed {
1403 path: path.snapshot(),
1404 scheme: parsed.scheme().to_string(),
1405 });
1406 }
1407 if let Some(allowed) = &spec.allowed_hosts {
1408 match parsed.host_str() {
1409 Some(host)
1410 if allowed
1411 .iter()
1412 .any(|allowed_host| allowed_host.eq_ignore_ascii_case(host)) => {}
1413 Some(host) => errors.push(ValueError::UrlHostNotAllowed {
1414 path: path.snapshot(),
1415 host: host.to_string(),
1416 }),
1417 None => errors.push(ValueError::UrlHostMissing {
1418 path: path.snapshot(),
1419 }),
1420 }
1421 }
1422}
1423
1424fn check_quantity(
1425 spec: &QuantitySpec,
1426 value: &QuantityValue,
1427 path: &mut ValuePath,
1428 errors: &mut Vec<ValueError>,
1429) {
1430 let unit_ok = if spec.allowed_suffixes.is_empty() {
1431 value.unit == spec.base_unit
1432 } else {
1433 spec.allowed_suffixes.iter().any(|s| s == &value.unit)
1434 };
1435 if !unit_ok {
1436 errors.push(ValueError::QuantityUnitNotAllowed {
1437 path: path.snapshot(),
1438 unit: value.unit.clone(),
1439 });
1440 return;
1443 }
1444
1445 if let Some(min) = &spec.min {
1446 match quantity_le_checked(min, value) {
1447 Some(true) => {}
1448 Some(false) => errors.push(ValueError::QuantityOutOfRange {
1449 path: path.snapshot(),
1450 reason: format!("value is below min `{}`", render_quantity(min)),
1451 }),
1452 None => errors.push(ValueError::QuantityOutOfRange {
1453 path: path.snapshot(),
1454 reason: "overflow".to_string(),
1455 }),
1456 }
1457 }
1458 if let Some(max) = &spec.max {
1459 match quantity_le_checked(value, max) {
1460 Some(true) => {}
1461 Some(false) => errors.push(ValueError::QuantityOutOfRange {
1462 path: path.snapshot(),
1463 reason: format!("value is above max `{}`", render_quantity(max)),
1464 }),
1465 None => errors.push(ValueError::QuantityOutOfRange {
1466 path: path.snapshot(),
1467 reason: "overflow".to_string(),
1468 }),
1469 }
1470 }
1471}
1472
1473#[cfg(not(all(feature = "guest", not(feature = "host"))))]
1474fn check_secret(
1475 spec: &SecretSpec,
1476 payload: &SecretValuePayload,
1477 path: &mut ValuePath,
1478 errors: &mut Vec<ValueError>,
1479) {
1480 if let Some(expected) = &spec.category
1481 && payload.category.as_ref() != Some(expected)
1482 {
1483 errors.push(ValueError::SecretCategoryMismatch {
1484 path: path.snapshot(),
1485 expected: expected.clone(),
1486 found: payload.category.clone(),
1487 });
1488 }
1489}
1490
1491#[cfg(not(all(feature = "guest", not(feature = "host"))))]
1492fn check_quota_token(
1493 spec: &QuotaTokenSpec,
1494 payload: &QuotaTokenValuePayload,
1495 path: &mut ValuePath,
1496 errors: &mut Vec<ValueError>,
1497) {
1498 if let Some(expected) = &spec.resource_name
1499 && expected != &payload.resource_name
1500 {
1501 errors.push(ValueError::QuotaTokenResourceMismatch {
1502 path: path.snapshot(),
1503 expected: expected.clone(),
1504 found: payload.resource_name.clone(),
1505 });
1506 }
1507}
1508
1509#[cfg(not(all(feature = "guest", not(feature = "host"))))]
1510fn check_permission_card(
1511 spec: &PermissionCardSpec,
1512 payload: &PermissionCardValuePayload,
1513 path: &mut ValuePath,
1514 errors: &mut Vec<ValueError>,
1515) {
1516 if spec.polymorphic != payload.polymorphic {
1517 errors.push(ValueError::PermissionCardPolymorphicMismatch {
1518 path: path.snapshot(),
1519 expected: spec.polymorphic,
1520 found: payload.polymorphic,
1521 });
1522 }
1523}
1524
1525fn discriminator_matches(
1526 index: &ValidationContext,
1527 branch: &UnionBranch,
1528 body: &SchemaValue,
1529) -> bool {
1530 match &branch.discriminator {
1531 DiscriminatorRule::Prefix { prefix } => string_view(index, &branch.body, body)
1532 .map(|s| s.starts_with(prefix.as_str()))
1533 .unwrap_or(false),
1534 DiscriminatorRule::Suffix { suffix } => string_view(index, &branch.body, body)
1535 .map(|s| s.ends_with(suffix.as_str()))
1536 .unwrap_or(false),
1537 DiscriminatorRule::Contains { substring } => string_view(index, &branch.body, body)
1538 .map(|s| s.contains(substring.as_str()))
1539 .unwrap_or(false),
1540 #[cfg(not(feature = "regex"))]
1541 DiscriminatorRule::Regex { .. } => false,
1542 #[cfg(feature = "regex")]
1543 DiscriminatorRule::Regex { regex } => {
1544 let Some(s) = string_view(index, &branch.body, body) else {
1545 return false;
1546 };
1547 match index.regexes.get(regex.as_str()) {
1548 Some(Some(compiled)) => compiled.is_match(s),
1549 _ => false,
1550 }
1551 }
1552 DiscriminatorRule::FieldEquals(field_disc) => {
1553 let Some(record) = record_view(index, &branch.body, body) else {
1554 return false;
1555 };
1556 let pos = record
1557 .field_names
1558 .iter()
1559 .position(|n| n == &field_disc.field_name);
1560 let Some(pos) = pos else {
1561 return false;
1562 };
1563 match &field_disc.literal {
1564 None => true,
1565 Some(lit) => {
1566 let v = &record.values[pos];
1567 matches!(v, SchemaValue::String(s) if s == lit)
1568 || matches!(v, SchemaValue::Text(t) if &t.text == lit)
1569 || matches!(v, SchemaValue::Url { url } if url == lit)
1570 || matches!(v, SchemaValue::Path { path } if path == lit)
1571 }
1572 }
1573 }
1574 DiscriminatorRule::FieldAbsent { field_name } => {
1575 let Some(record) = record_view(index, &branch.body, body) else {
1576 return false;
1577 };
1578 !record.field_names.iter().any(|n| n == field_name)
1579 }
1580 }
1581}
1582
1583fn string_view<'a>(index: &GraphIndex, ty: &SchemaType, value: &'a SchemaValue) -> Option<&'a str> {
1584 match (resolve(index, ty), value) {
1585 (Some(_), SchemaValue::String(s)) => Some(s.as_str()),
1586 (Some(_), SchemaValue::Text(t)) => Some(t.text.as_str()),
1587 (Some(_), SchemaValue::Url { url }) => Some(url.as_str()),
1588 (Some(_), SchemaValue::Path { path }) => Some(path.as_str()),
1589 _ => None,
1590 }
1591}
1592
1593struct RecordView<'a> {
1594 field_names: Vec<String>,
1595 values: &'a [SchemaValue],
1596}
1597
1598fn record_view<'a>(
1599 index: &GraphIndex,
1600 ty: &SchemaType,
1601 value: &'a SchemaValue,
1602) -> Option<RecordView<'a>> {
1603 let resolved = resolve_record(index, ty)?;
1604 match value {
1605 SchemaValue::Record { fields } if fields.len() == resolved.len() => Some(RecordView {
1606 field_names: resolved,
1607 values: fields.as_slice(),
1608 }),
1609 _ => None,
1610 }
1611}
1612
1613fn resolve<'a>(index: &GraphIndex<'a>, ty: &'a SchemaType) -> Option<&'a SchemaType> {
1614 let mut current = ty;
1615 let max_hops = index.defs_len();
1619 let mut hops = 0usize;
1620 loop {
1621 match current {
1622 SchemaType::Ref { id, .. } => {
1623 if hops > max_hops {
1624 return None;
1625 }
1626 hops += 1;
1627 {
1628 let def = index.lookup(id)?;
1629 current = &def.body
1630 }
1631 }
1632 other => return Some(other),
1633 }
1634 }
1635}
1636
1637fn resolve_record(index: &GraphIndex, ty: &SchemaType) -> Option<Vec<String>> {
1638 let resolved = resolve(index, ty)?;
1639 match resolved {
1640 SchemaType::Record { fields, .. } => Some(fields.iter().map(|f| f.name.clone()).collect()),
1641 _ => None,
1642 }
1643}
1644
1645fn file_extension(p: &str) -> Option<&str> {
1646 let name = p.rsplit('/').next()?;
1647 let (_, ext) = name.rsplit_once('.')?;
1648 if ext.is_empty() { None } else { Some(ext) }
1649}
1650
1651fn render_quantity(q: &QuantityValue) -> String {
1652 format!("{}*10^(-{}) {}", q.mantissa, q.scale, q.unit)
1653}
1654
1655fn quantity_le_checked(a: &QuantityValue, b: &QuantityValue) -> Option<bool> {
1656 let common = a.scale.max(b.scale);
1657 let a_shift = (common - a.scale).max(0) as u32;
1658 let b_shift = (common - b.scale).max(0) as u32;
1659 let ten: i128 = 10;
1660 let a_factor = ten.checked_pow(a_shift)?;
1661 let b_factor = ten.checked_pow(b_shift)?;
1662 let a_canon = (a.mantissa as i128).checked_mul(a_factor)?;
1663 let b_canon = (b.mantissa as i128).checked_mul(b_factor)?;
1664 Some(a_canon <= b_canon)
1665}