1use crate::ir::{
4 key_string, FieldFormat, FieldType, Inventory, Object, Schema, SourceLocation, TypeName, Uid,
5};
6use ipnet::IpNet;
7use regex::Regex;
8use serde_json::Value;
9use std::collections::{BTreeMap, BTreeSet};
10use std::fmt;
11use std::net::IpAddr;
12use std::sync::OnceLock;
13use thiserror::Error;
14
15#[derive(Debug, Error, Clone, PartialEq, Eq)]
17pub enum ValidationError {
18 #[error("duplicate uid: {0}")]
19 DuplicateUid(Uid),
20 #[error("duplicate key: {0}")]
21 DuplicateKey(String),
22 #[error("missing type on object")]
23 MissingType,
24 #[error("missing key on object")]
25 MissingKey,
26 #[error("missing key field {type_name}.{field}")]
27 MissingKeyField { type_name: String, field: String },
28 #[error("extra key field {type_name}.{field}")]
29 ExtraKeyField { type_name: String, field: String },
30 #[error("missing attr field {type_name}.{field}")]
31 MissingAttrField { type_name: String, field: String },
32 #[error("extra attr field {type_name}.{field}")]
33 ExtraAttrField { type_name: String, field: String },
34 #[error("invalid value for {field}: expected {expected}, got {actual}")]
35 InvalidValue {
36 field: String,
37 expected: String,
38 actual: String,
39 },
40 #[error("unknown type: {0}")]
41 UnknownType(String),
42 #[error("missing reference {field} -> {target}")]
43 MissingReference { field: String, target: Uid },
44 #[error("reference type mismatch {field} -> {target} (expected {expected}, got {actual})")]
45 ReferenceTypeMismatch {
46 field: String,
47 target: Uid,
48 expected: String,
49 actual: String,
50 },
51}
52
53impl ValidationError {
54 pub fn uid(&self) -> Option<Uid> {
56 match self {
57 ValidationError::DuplicateUid(uid) => Some(*uid),
58 ValidationError::MissingReference { target, .. } => Some(*target),
59 ValidationError::ReferenceTypeMismatch { target, .. } => Some(*target),
60 _ => None,
61 }
62 }
63
64 pub fn key_hint(&self) -> Option<String> {
66 match self {
67 ValidationError::DuplicateKey(key) => {
68 if let Some((_, k)) = key.split_once("::") {
69 Some(k.to_string())
70 } else {
71 Some(key.clone())
72 }
73 }
74 _ => None,
75 }
76 }
77
78 pub fn type_hint(&self) -> Option<String> {
80 match self {
81 ValidationError::UnknownType(t) => Some(t.clone()),
82 ValidationError::MissingKeyField { type_name, .. }
83 | ValidationError::ExtraKeyField { type_name, .. }
84 | ValidationError::MissingAttrField { type_name, .. }
85 | ValidationError::ExtraAttrField { type_name, .. } => Some(type_name.clone()),
86 ValidationError::InvalidValue { field, .. } => {
87 field.split('.').next().map(|s| s.to_string())
88 }
89 ValidationError::MissingReference { field, .. }
90 | ValidationError::ReferenceTypeMismatch { field, .. } => {
91 field.split('.').next().map(|s| s.to_string())
92 }
93 ValidationError::DuplicateKey(key) => key.split("::").next().map(|s| s.to_string()),
94 _ => None,
95 }
96 }
97}
98
99#[derive(Debug, Clone)]
101pub struct LocatedError {
102 pub error: ValidationError,
103 pub source: Option<SourceLocation>,
104}
105
106impl LocatedError {
107 pub fn new(error: ValidationError) -> Self {
108 Self {
109 error,
110 source: None,
111 }
112 }
113
114 pub fn with_source(error: ValidationError, source: Option<SourceLocation>) -> Self {
115 Self { error, source }
116 }
117}
118
119impl fmt::Display for LocatedError {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 if let Some(source) = &self.source {
122 write!(f, "{}: {}", source, self.error)
123 } else {
124 write!(f, "{}", self.error)
125 }
126 }
127}
128
129#[derive(Debug, Default, Clone)]
131pub struct ValidationReport {
132 pub errors: Vec<ValidationError>,
133}
134
135impl ValidationReport {
136 pub fn is_ok(&self) -> bool {
138 self.errors.is_empty()
139 }
140
141 pub fn is_err(&self) -> bool {
143 !self.errors.is_empty()
144 }
145
146 pub fn with_sources(self, objects: &[Object]) -> Vec<LocatedError> {
151 let uid_to_source: BTreeMap<Uid, Option<SourceLocation>> =
153 objects.iter().map(|o| (o.uid, o.source.clone())).collect();
154 let key_to_source: BTreeMap<String, Option<SourceLocation>> = objects
155 .iter()
156 .map(|o| {
157 let key = format!("{}::{}", o.type_name, key_string(&o.key));
158 (key, o.source.clone())
159 })
160 .collect();
161 let type_to_source: BTreeMap<String, Option<SourceLocation>> = objects
162 .iter()
163 .filter_map(|o| o.source.clone().map(|s| (o.type_name.to_string(), Some(s))))
164 .collect();
165
166 self.errors
167 .into_iter()
168 .map(|error| {
169 let source = error
170 .uid()
171 .and_then(|uid| uid_to_source.get(&uid).cloned().flatten())
172 .or_else(|| {
173 error.key_hint().and_then(|_| {
174 if let ValidationError::DuplicateKey(key) = &error {
176 key_to_source.get(key).cloned().flatten()
177 } else {
178 None
179 }
180 })
181 })
182 .or_else(|| {
183 error
185 .type_hint()
186 .and_then(|t| type_to_source.get(&t).cloned().flatten())
187 });
188 LocatedError::with_source(error, source)
189 })
190 .collect()
191 }
192}
193
194pub fn validate_inventory(inventory: &Inventory) -> ValidationReport {
196 let mut report = ValidationReport::default();
197 let mut seen_uids = BTreeSet::new();
198 let mut seen_keys = BTreeSet::new();
199 let mut uid_to_type = BTreeMap::new();
200
201 for object in &inventory.objects {
202 if object.key.is_empty() {
203 report.errors.push(ValidationError::MissingKey);
204 }
205 if object.type_name.is_empty() {
206 report.errors.push(ValidationError::MissingType);
207 }
208 if !seen_uids.insert(object.uid) {
209 report
210 .errors
211 .push(ValidationError::DuplicateUid(object.uid));
212 }
213 let key = format!("{}::{}", object.type_name, key_string(&object.key));
214 if !seen_keys.insert(key.clone()) {
215 report.errors.push(ValidationError::DuplicateKey(key));
216 }
217 uid_to_type.insert(object.uid, object.type_name.clone());
218 }
219
220 validate_schema_types(&inventory.schema, &inventory.objects, &mut report);
221 for object in &inventory.objects {
222 validate_object(object, &inventory.schema, &uid_to_type, &mut report);
223 }
224
225 report
226}
227
228fn validate_schema_types(schema: &Schema, objects: &[Object], report: &mut ValidationReport) {
229 for object in objects {
230 if !schema.types.contains_key(object.type_name.as_str()) {
231 report
232 .errors
233 .push(ValidationError::UnknownType(object.type_name.to_string()));
234 }
235 }
236}
237
238fn validate_object(
239 object: &Object,
240 schema: &Schema,
241 uid_to_type: &BTreeMap<Uid, TypeName>,
242 report: &mut ValidationReport,
243) {
244 let Some(type_schema) = schema.types.get(object.type_name.as_str()) else {
245 return;
246 };
247
248 validate_key_fields(object, type_schema, uid_to_type, report);
249 validate_attr_fields(object, type_schema, uid_to_type, report);
250}
251
252fn validate_key_fields(
253 object: &Object,
254 type_schema: &crate::ir::TypeSchema,
255 uid_to_type: &BTreeMap<Uid, TypeName>,
256 report: &mut ValidationReport,
257) {
258 for (field, field_schema) in &type_schema.key {
259 let Some(value) = object.key.get(field) else {
260 report.errors.push(ValidationError::MissingKeyField {
261 type_name: object.type_name.to_string(),
262 field: field.to_string(),
263 });
264 continue;
265 };
266 validate_field_value(
267 &object.type_name,
268 &format!("key.{field}"),
269 field_schema,
270 value,
271 uid_to_type,
272 report,
273 );
274 }
275
276 for field in object.key.keys() {
277 if !type_schema.key.contains_key(field) {
278 report.errors.push(ValidationError::ExtraKeyField {
279 type_name: object.type_name.to_string(),
280 field: field.to_string(),
281 });
282 }
283 }
284}
285
286fn validate_attr_fields(
287 object: &Object,
288 type_schema: &crate::ir::TypeSchema,
289 uid_to_type: &BTreeMap<Uid, TypeName>,
290 report: &mut ValidationReport,
291) {
292 for (field, field_schema) in &type_schema.fields {
293 let Some(value) = object.attrs.get(field) else {
294 if field_schema.required {
295 report.errors.push(ValidationError::MissingAttrField {
296 type_name: object.type_name.to_string(),
297 field: field.to_string(),
298 });
299 }
300 continue;
301 };
302 validate_field_value(
303 &object.type_name,
304 field,
305 field_schema,
306 value,
307 uid_to_type,
308 report,
309 );
310 }
311
312 for field in object.attrs.keys() {
313 if !type_schema.fields.contains_key(field) {
314 report.errors.push(ValidationError::ExtraAttrField {
315 type_name: object.type_name.to_string(),
316 field: field.to_string(),
317 });
318 }
319 }
320}
321
322fn validate_field_value(
323 type_name: &TypeName,
324 field: &str,
325 field_schema: &crate::ir::FieldSchema,
326 value: &Value,
327 uid_to_type: &BTreeMap<Uid, TypeName>,
328 report: &mut ValidationReport,
329) {
330 if value.is_null() {
331 if field_schema.nullable {
332 return;
333 }
334 report.errors.push(ValidationError::InvalidValue {
335 field: format!("{type_name}.{field}"),
336 expected: field_type_label(&field_schema.r#type),
337 actual: "null".to_string(),
338 });
339 return;
340 }
341
342 match &field_schema.r#type {
343 FieldType::Ref { target } => {
344 validate_ref(type_name, field, target, value, uid_to_type, report);
345 }
346 FieldType::ListRef { target } => {
347 if let Some(entries) = value.as_array() {
348 for entry in entries {
349 validate_ref(type_name, field, target, entry, uid_to_type, report);
350 }
351 } else {
352 report.errors.push(ValidationError::InvalidValue {
353 field: format!("{type_name}.{field}"),
354 expected: "list_ref".to_string(),
355 actual: value_type_label(value),
356 });
357 }
358 }
359 FieldType::List { item } => {
360 if let Some(entries) = value.as_array() {
361 for entry in entries {
362 let schema = crate::ir::FieldSchema {
363 r#type: (**item).clone(),
364 required: true,
365 nullable: false,
366 description: None,
367 format: None,
368 pattern: None,
369 };
370 validate_field_value(type_name, field, &schema, entry, uid_to_type, report);
371 }
372 } else {
373 report.errors.push(ValidationError::InvalidValue {
374 field: format!("{type_name}.{field}"),
375 expected: "list".to_string(),
376 actual: value_type_label(value),
377 });
378 }
379 }
380 FieldType::Map { value: inner } => {
381 if let Some(entries) = value.as_object() {
382 for entry in entries.values() {
383 let schema = crate::ir::FieldSchema {
384 r#type: (**inner).clone(),
385 required: true,
386 nullable: false,
387 description: None,
388 format: None,
389 pattern: None,
390 };
391 validate_field_value(type_name, field, &schema, entry, uid_to_type, report);
392 }
393 } else {
394 report.errors.push(ValidationError::InvalidValue {
395 field: format!("{type_name}.{field}"),
396 expected: "map".to_string(),
397 actual: value_type_label(value),
398 });
399 }
400 }
401 FieldType::Enum { values } => {
402 if let Some(raw) = value.as_str() {
403 if !values.contains(&raw.to_string()) {
404 report.errors.push(ValidationError::InvalidValue {
405 field: format!("{type_name}.{field}"),
406 expected: format!("enum({})", values.join("|")),
407 actual: raw.to_string(),
408 });
409 }
410 } else {
411 report.errors.push(ValidationError::InvalidValue {
412 field: format!("{type_name}.{field}"),
413 expected: "enum".to_string(),
414 actual: value_type_label(value),
415 });
416 }
417 }
418 _ => {
419 if !value_matches_type(value, &field_schema.r#type) {
420 report.errors.push(ValidationError::InvalidValue {
421 field: format!("{type_name}.{field}"),
422 expected: field_type_label(&field_schema.r#type),
423 actual: value_type_label(value),
424 });
425 }
426 }
427 }
428
429 validate_string_constraints(type_name, field, field_schema, value, report);
430}
431
432fn parse_uid(value: &Value) -> Option<Uid> {
433 let raw = value.as_str()?;
434 Uid::parse_str(raw).ok()
435}
436
437fn validate_ref(
438 type_name: &TypeName,
439 field: &str,
440 target: &str,
441 value: &Value,
442 uid_to_type: &BTreeMap<Uid, TypeName>,
443 report: &mut ValidationReport,
444) {
445 let Some(uid) = parse_uid(value) else {
446 report.errors.push(ValidationError::InvalidValue {
447 field: format!("{type_name}.{field}"),
448 expected: "uuid".to_string(),
449 actual: value_type_label(value),
450 });
451 return;
452 };
453 let Some(actual) = uid_to_type.get(&uid) else {
454 report.errors.push(ValidationError::MissingReference {
455 field: format!("{type_name}.{field}"),
456 target: uid,
457 });
458 return;
459 };
460 if actual.as_str() != target {
461 report.errors.push(ValidationError::ReferenceTypeMismatch {
462 field: format!("{type_name}.{field}"),
463 target: uid,
464 expected: target.to_string(),
465 actual: actual.to_string(),
466 });
467 }
468}
469
470fn validate_string_constraints(
471 type_name: &TypeName,
472 field: &str,
473 field_schema: &crate::ir::FieldSchema,
474 value: &Value,
475 report: &mut ValidationReport,
476) {
477 if field_schema.format.is_none() && field_schema.pattern.is_none() {
478 return;
479 }
480
481 let Some(raw) = value.as_str() else {
482 report.errors.push(ValidationError::InvalidValue {
483 field: format!("{type_name}.{field}"),
484 expected: "string".to_string(),
485 actual: value_type_label(value),
486 });
487 return;
488 };
489
490 if let Some(format) = &field_schema.format {
491 if !matches_format(format, raw) {
492 report.errors.push(ValidationError::InvalidValue {
493 field: format!("{type_name}.{field}"),
494 expected: format_label(format),
495 actual: raw.to_string(),
496 });
497 }
498 }
499
500 if let Some(pattern) = &field_schema.pattern {
501 match Regex::new(pattern) {
502 Ok(regex) => {
503 if !regex.is_match(raw) {
504 report.errors.push(ValidationError::InvalidValue {
505 field: format!("{type_name}.{field}"),
506 expected: format!("pattern({pattern})"),
507 actual: raw.to_string(),
508 });
509 }
510 }
511 Err(err) => {
512 report.errors.push(ValidationError::InvalidValue {
513 field: format!("{type_name}.{field}"),
514 expected: format!("pattern({pattern})"),
515 actual: format!("invalid pattern: {err}"),
516 });
517 }
518 }
519 }
520}
521
522fn slug_regex() -> &'static Regex {
523 static RE: OnceLock<Regex> = OnceLock::new();
524 RE.get_or_init(|| Regex::new(r"^[a-z0-9]+(?:[a-z0-9_-]*[a-z0-9])?$").unwrap())
525}
526
527fn mac_regex() -> &'static Regex {
528 static RE: OnceLock<Regex> = OnceLock::new();
529 RE.get_or_init(|| Regex::new(r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$").unwrap())
530}
531
532fn matches_format(format: &FieldFormat, raw: &str) -> bool {
533 match format {
534 FieldFormat::Slug => slug_regex().is_match(raw),
535 FieldFormat::IpAddress => raw.parse::<IpAddr>().is_ok(),
536 FieldFormat::Cidr | FieldFormat::Prefix => raw.parse::<IpNet>().is_ok(),
537 FieldFormat::Mac => mac_regex().is_match(raw),
538 FieldFormat::Uuid => Uid::parse_str(raw).is_ok(),
539 }
540}
541
542fn format_label(format: &FieldFormat) -> String {
543 match format {
544 FieldFormat::Slug => "format(slug)".to_string(),
545 FieldFormat::IpAddress => "format(ip_address)".to_string(),
546 FieldFormat::Cidr => "format(cidr)".to_string(),
547 FieldFormat::Prefix => "format(prefix)".to_string(),
548 FieldFormat::Mac => "format(mac)".to_string(),
549 FieldFormat::Uuid => "format(uuid)".to_string(),
550 }
551}
552
553fn value_matches_type(value: &Value, field_type: &FieldType) -> bool {
554 match field_type {
555 FieldType::String
556 | FieldType::Text
557 | FieldType::Date
558 | FieldType::Datetime
559 | FieldType::Time
560 | FieldType::IpAddress
561 | FieldType::Cidr
562 | FieldType::Prefix
563 | FieldType::Mac
564 | FieldType::Slug => value.is_string(),
565 FieldType::Uuid => value
566 .as_str()
567 .map(|raw| Uid::parse_str(raw).is_ok())
568 .unwrap_or(false),
569 FieldType::Int => value.is_i64() || value.is_u64(),
570 FieldType::Float => value.as_f64().is_some() || value.is_i64() || value.is_u64(),
571 FieldType::Bool => value.is_boolean(),
572 FieldType::Json => true,
573 FieldType::Enum { .. } => value.is_string(),
574 FieldType::List { .. } => value.is_array(),
575 FieldType::Map { .. } => value.is_object(),
576 FieldType::Ref { .. } | FieldType::ListRef { .. } => true,
577 }
578}
579
580fn field_type_label(field_type: &FieldType) -> String {
581 match field_type {
582 FieldType::String => "string".to_string(),
583 FieldType::Text => "text".to_string(),
584 FieldType::Int => "int".to_string(),
585 FieldType::Float => "float".to_string(),
586 FieldType::Bool => "bool".to_string(),
587 FieldType::Uuid => "uuid".to_string(),
588 FieldType::Date => "date".to_string(),
589 FieldType::Datetime => "datetime".to_string(),
590 FieldType::Time => "time".to_string(),
591 FieldType::Json => "json".to_string(),
592 FieldType::IpAddress => "ip_address".to_string(),
593 FieldType::Cidr => "cidr".to_string(),
594 FieldType::Prefix => "prefix".to_string(),
595 FieldType::Mac => "mac".to_string(),
596 FieldType::Slug => "slug".to_string(),
597 FieldType::Enum { .. } => "enum".to_string(),
598 FieldType::List { .. } => "list".to_string(),
599 FieldType::Map { .. } => "map".to_string(),
600 FieldType::Ref { target } => format!("ref({target})"),
601 FieldType::ListRef { target } => format!("list_ref({target})"),
602 }
603}
604
605fn value_type_label(value: &Value) -> String {
606 match value {
607 Value::Null => "null".to_string(),
608 Value::Bool(_) => "bool".to_string(),
609 Value::Number(_) => "number".to_string(),
610 Value::String(_) => "string".to_string(),
611 Value::Array(_) => "array".to_string(),
612 Value::Object(_) => "object".to_string(),
613 }
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619 use crate::ir::{FieldSchema, FieldType, JsonMap, Key, Object, Schema, TypeName, TypeSchema};
620 use serde_json::json;
621 use std::collections::BTreeMap;
622 use uuid::Uuid;
623
624 fn uid(value: u128) -> Uid {
625 Uuid::from_u128(value)
626 }
627
628 #[test]
629 fn detects_duplicate_keys() {
630 let mut key = BTreeMap::new();
631 key.insert("slug".to_string(), serde_json::json!("fra1"));
632 let key = Key::from(key);
633 let type_schema = TypeSchema {
634 key: BTreeMap::from([(
635 "slug".to_string(),
636 FieldSchema {
637 r#type: FieldType::Slug,
638 required: true,
639 nullable: false,
640 description: None,
641 format: None,
642 pattern: None,
643 },
644 )]),
645 fields: BTreeMap::new(),
646 };
647 let objects = vec![
648 Object::new(
649 uid(1),
650 TypeName::new("site"),
651 key.clone(),
652 JsonMap::default(),
653 )
654 .unwrap(),
655 Object::new(uid(2), TypeName::new("site"), key, JsonMap::default()).unwrap(),
656 ];
657 let report = validate_inventory(&Inventory {
658 schema: Schema {
659 types: BTreeMap::from([("site".to_string(), type_schema)]),
660 },
661 objects,
662 });
663 assert!(report
664 .errors
665 .iter()
666 .any(|err| matches!(err, ValidationError::DuplicateKey(_))));
667 }
668
669 #[test]
670 fn detects_missing_key() {
671 let objects = vec![Object {
672 uid: uid(30),
673 type_name: TypeName::new("site"),
674 key: Key::default(),
675 attrs: JsonMap::default(),
676 source: None,
677 }];
678 let report = validate_inventory(&Inventory {
679 schema: Schema {
680 types: BTreeMap::from([(
681 "site".to_string(),
682 TypeSchema {
683 key: BTreeMap::new(),
684 fields: BTreeMap::new(),
685 },
686 )]),
687 },
688 objects,
689 });
690 assert!(report
691 .errors
692 .iter()
693 .any(|err| matches!(err, ValidationError::MissingKey)));
694 }
695
696 #[test]
697 fn detects_missing_kind() {
698 let mut key = BTreeMap::new();
699 key.insert("slug".to_string(), serde_json::json!("fra1"));
700 let objects = vec![Object {
701 uid: uid(31),
702 type_name: TypeName::new(""),
703 key: Key::from(key),
704 attrs: JsonMap::default(),
705 source: None,
706 }];
707 let report = validate_inventory(&Inventory {
708 schema: Schema {
709 types: BTreeMap::new(),
710 },
711 objects,
712 });
713 assert!(report
714 .errors
715 .iter()
716 .any(|err| matches!(err, ValidationError::MissingType)));
717 }
718
719 #[test]
720 fn detects_unknown_type() {
721 let mut key = BTreeMap::new();
722 key.insert("slug".to_string(), serde_json::json!("leaf01"));
723 let objects = vec![Object::new(
724 uid(40),
725 TypeName::new("device"),
726 Key::from(key),
727 JsonMap::default(),
728 )
729 .unwrap()];
730 let schema = Schema {
731 types: BTreeMap::new(),
732 };
733 let report = validate_inventory(&Inventory { schema, objects });
734 assert!(report
735 .errors
736 .iter()
737 .any(|err| matches!(err, ValidationError::UnknownType(_))));
738 }
739
740 #[test]
741 fn detects_missing_references_with_schema() {
742 let mut key_fields = BTreeMap::new();
743 key_fields.insert(
744 "slug".to_string(),
745 FieldSchema {
746 r#type: FieldType::Slug,
747 required: true,
748 nullable: false,
749 description: None,
750 format: None,
751 pattern: None,
752 },
753 );
754 let mut fields = BTreeMap::new();
755 fields.insert(
756 "owner".to_string(),
757 FieldSchema {
758 r#type: FieldType::Ref {
759 target: "person".to_string(),
760 },
761 required: false,
762 nullable: false,
763 description: None,
764 format: None,
765 pattern: None,
766 },
767 );
768 let mut types = BTreeMap::new();
769 types.insert(
770 "device".to_string(),
771 TypeSchema {
772 key: key_fields,
773 fields,
774 },
775 );
776 let schema = Schema { types };
777
778 let mut attrs = BTreeMap::new();
779 attrs.insert(
780 "owner".to_string(),
781 serde_json::json!(Uuid::from_u128(99).to_string()),
782 );
783 let mut key = BTreeMap::new();
784 key.insert("slug".to_string(), serde_json::json!("leaf01"));
785 let objects = vec![Object::new(
786 uid(41),
787 TypeName::new("device"),
788 Key::from(key),
789 attrs.into(),
790 )
791 .unwrap()];
792 let report = validate_inventory(&Inventory { schema, objects });
793 assert!(report
794 .errors
795 .iter()
796 .any(|err| matches!(err, ValidationError::MissingReference { .. })));
797 }
798
799 #[test]
800 fn test_field_value_validation() {
801 let uid_to_type = BTreeMap::from([(uid(1), TypeName::new("target"))]);
802 let mut report = ValidationReport::default();
803
804 let schema = FieldSchema {
806 r#type: FieldType::Int,
807 required: true,
808 nullable: false,
809 description: None,
810 format: None,
811 pattern: None,
812 };
813 validate_field_value(
814 &TypeName::new("test"),
815 "field",
816 &schema,
817 &json!("not-int"),
818 &uid_to_type,
819 &mut report,
820 );
821 assert!(report
822 .errors
823 .iter()
824 .any(|e| matches!(e, ValidationError::InvalidValue { .. })));
825
826 let schema = FieldSchema {
828 r#type: FieldType::Enum {
829 values: vec!["a".to_string(), "b".to_string()],
830 },
831 required: true,
832 nullable: false,
833 description: None,
834 format: None,
835 pattern: None,
836 };
837 report.errors.clear();
838 validate_field_value(
839 &TypeName::new("test"),
840 "field",
841 &schema,
842 &json!("c"),
843 &uid_to_type,
844 &mut report,
845 );
846 assert!(report
847 .errors
848 .iter()
849 .any(|e| matches!(e, ValidationError::InvalidValue { .. })));
850
851 let schema = FieldSchema {
853 r#type: FieldType::Ref {
854 target: "wrong".to_string(),
855 },
856 required: true,
857 nullable: false,
858 description: None,
859 format: None,
860 pattern: None,
861 };
862 report.errors.clear();
863 validate_field_value(
864 &TypeName::new("test"),
865 "field",
866 &schema,
867 &json!(uid(1).to_string()),
868 &uid_to_type,
869 &mut report,
870 );
871 assert!(report
872 .errors
873 .iter()
874 .any(|e| matches!(e, ValidationError::ReferenceTypeMismatch { .. })));
875
876 let schema = FieldSchema {
878 r#type: FieldType::ListRef {
879 target: "target".to_string(),
880 },
881 required: true,
882 nullable: false,
883 description: None,
884 format: None,
885 pattern: None,
886 };
887 report.errors.clear();
888 validate_field_value(
889 &TypeName::new("test"),
890 "field",
891 &schema,
892 &json!([uid(1).to_string()]),
893 &uid_to_type,
894 &mut report,
895 );
896 assert!(report.errors.is_empty());
897
898 let schema = FieldSchema {
900 r#type: FieldType::Map {
901 value: Box::new(FieldType::Int),
902 },
903 required: true,
904 nullable: false,
905 description: None,
906 format: None,
907 pattern: None,
908 };
909 report.errors.clear();
910 validate_field_value(
911 &TypeName::new("test"),
912 "field",
913 &schema,
914 &json!({"a": 1, "b": "not-int"}),
915 &uid_to_type,
916 &mut report,
917 );
918 assert!(report
919 .errors
920 .iter()
921 .any(|e| matches!(e, ValidationError::InvalidValue { .. })));
922
923 let schema = FieldSchema {
925 r#type: FieldType::Uuid,
926 required: true,
927 nullable: false,
928 description: None,
929 format: None,
930 pattern: None,
931 };
932 report.errors.clear();
933 validate_field_value(
934 &TypeName::new("test"),
935 "field",
936 &schema,
937 &json!("not-a-uuid"),
938 &uid_to_type,
939 &mut report,
940 );
941 assert!(report
942 .errors
943 .iter()
944 .any(|e| matches!(e, ValidationError::InvalidValue { .. })));
945
946 let schema = FieldSchema {
948 r#type: FieldType::List {
949 item: Box::new(FieldType::Ref {
950 target: "target".to_string(),
951 }),
952 },
953 required: true,
954 nullable: false,
955 description: None,
956 format: None,
957 pattern: None,
958 };
959 report.errors.clear();
960 validate_field_value(
961 &TypeName::new("test"),
962 "field",
963 &schema,
964 &json!([uid(1).to_string()]),
965 &uid_to_type,
966 &mut report,
967 );
968 assert!(report.errors.is_empty());
969 }
970}