ifc_validate/type_check/defined.rs
1//! Values checked against the defined type declared for their slot.
2
3use ifc_model::Value;
4use ifc_schema::Schema;
5
6use super::enumeration;
7use super::scalar::{describe_value, primitive_of, FixedWidth};
8use super::select;
9
10/// Why a value does not fit its declared type.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum Mismatch {
13 /// The written form is not what the primitive accepts.
14 Primitive {
15 /// What the schema expects, in words.
16 expected: &'static str,
17 /// What the file wrote, in words.
18 actual: &'static str,
19 },
20 /// An enumeration constant the schema does not declare.
21 EnumMember {
22 /// The constant as written.
23 member: String,
24 /// The members the schema declares.
25 declared: Vec<String>,
26 },
27 /// A typed value whose type is not a member of the declared SELECT.
28 SelectMember {
29 /// The wrapper type as written.
30 written: String,
31 /// The SELECT it had to belong to.
32 select: String,
33 },
34 /// A `STRING(n) FIXED` value written at the wrong width.
35 FixedWidth {
36 /// The width the schema fixes.
37 expected: usize,
38 /// The width actually written.
39 actual: usize,
40 },
41}
42
43/// Checks one value against one declared type name.
44///
45/// Returns `None` when the value fits, or when the schema gives no basis to
46/// judge it. "No basis" is deliberately not a finding: an unrecognized type
47/// token means this validator cannot check the slot, and reporting that per
48/// value would bury real defects under noise. The unchecked *rules* are
49/// counted once, in the where-rule registry.
50#[must_use]
51pub fn check(schema: &Schema, declared: &str, value: &Value) -> Option<Mismatch> {
52 match value {
53 // `$` and `*` carry no type; presence is `structure`'s concern.
54 Value::Null | Value::Derived => None,
55 Value::Enum(member) => match enumeration::is_member(schema, declared, member) {
56 Some(true) | None => None,
57 Some(false) => Some(Mismatch::EnumMember {
58 member: member.to_string(),
59 declared: enumeration::members(schema, declared)
60 .map(<[String]>::to_vec)
61 .unwrap_or_default(),
62 }),
63 },
64 Value::Typed { type_name, value } => {
65 // A typed wrapper in a SELECT slot must name a SELECT member.
66 if let Some(false) = select::accepts(schema, declared, type_name) {
67 return Some(Mismatch::SelectMember {
68 written: type_name.to_string(),
69 select: declared.to_string(),
70 });
71 }
72 // Otherwise the wrapper names the real type: check the payload
73 // against it rather than against the slot's declared type.
74 check(schema, type_name, value)
75 }
76 // A reference's target type is checked structurally, where the model
77 // is available to resolve it.
78 Value::Ref(_) => None,
79 // Aggregate shape is `structure::cardinality`'s concern; element
80 // types are not declared by the parser's flat type token.
81 Value::List(_) => None,
82 Value::Text(text) => {
83 // A fixed-width string is wrong at any other length, even when
84 // it is a perfectly good string. `IfcGloballyUniqueId` is
85 // `STRING(22) FIXED`; a 21-character GUID is malformed.
86 if let Some(width) = FixedWidth::from_resolved(&schema.resolve_defined(declared)) {
87 if !width.accepts(text) {
88 return Some(Mismatch::FixedWidth {
89 expected: width.0,
90 actual: text.chars().count(),
91 });
92 }
93 }
94 let primitive = primitive_of(schema, declared)?;
95 if primitive.accepts(value) {
96 None
97 } else {
98 Some(Mismatch::Primitive {
99 expected: primitive.describe(),
100 actual: describe_value(value),
101 })
102 }
103 }
104 other => {
105 let primitive = primitive_of(schema, declared)?;
106 if primitive.accepts(other) {
107 None
108 } else {
109 Some(Mismatch::Primitive {
110 expected: primitive.describe(),
111 actual: describe_value(other),
112 })
113 }
114 }
115 }
116}