1use std::collections::HashSet;
4
5use ifc_model::{Entity, EntityId, Model, Value};
6
7use crate::{ConstraintError, ConstraintResult};
8
9#[derive(Debug, Clone, Copy)]
11pub struct ConstraintView<'m> {
12 model: &'m Model,
13}
14
15impl<'m> ConstraintView<'m> {
16 #[must_use]
18 pub const fn new(model: &'m Model) -> Self {
19 Self { model }
20 }
21
22 pub(crate) const fn model(self) -> &'m Model {
23 self.model
24 }
25}
26
27pub(crate) fn wrong(expected: &'static str, entity: &Entity) -> ConstraintError {
28 ConstraintError::WrongEntityType {
29 expected,
30 actual: entity.type_name.to_string(),
31 }
32}
33
34pub(crate) fn invalid(
35 kind: &'static str,
36 id: EntityId,
37 attribute: &'static str,
38 value: &Value,
39) -> ConstraintError {
40 ConstraintError::InvalidValue {
41 entity: kind,
42 id,
43 attribute,
44 value: format!("{value:?}"),
45 }
46}
47
48pub(crate) fn optional_text<'m>(
49 kind: &'static str,
50 id: EntityId,
51 entity: &'m Entity,
52 slot: usize,
53 attribute: &'static str,
54) -> ConstraintResult<Option<&'m str>> {
55 match entity.attribute(slot) {
56 None | Some(Value::Null) => Ok(None),
57 Some(value) => value
58 .unwrap_typed()
59 .as_text()
60 .map(Some)
61 .ok_or_else(|| invalid(kind, id, attribute, value)),
62 }
63}
64
65pub(crate) fn required_text<'m>(
66 kind: &'static str,
67 id: EntityId,
68 entity: &'m Entity,
69 slot: usize,
70 attribute: &'static str,
71) -> ConstraintResult<&'m str> {
72 optional_text(kind, id, entity, slot, attribute)?.ok_or(ConstraintError::MissingAttribute {
73 entity: kind,
74 id,
75 attribute,
76 })
77}
78
79pub(crate) fn optional_ref(
80 kind: &'static str,
81 id: EntityId,
82 entity: &Entity,
83 slot: usize,
84 attribute: &'static str,
85) -> ConstraintResult<Option<EntityId>> {
86 match entity.attribute(slot) {
87 None | Some(Value::Null) => Ok(None),
88 Some(Value::Ref(target)) => Ok(Some(*target)),
89 Some(value) => Err(invalid(kind, id, attribute, value)),
90 }
91}
92
93pub(crate) fn required_ref(
94 kind: &'static str,
95 id: EntityId,
96 entity: &Entity,
97 slot: usize,
98 attribute: &'static str,
99) -> ConstraintResult<EntityId> {
100 optional_ref(kind, id, entity, slot, attribute)?.ok_or(ConstraintError::MissingAttribute {
101 entity: kind,
102 id,
103 attribute,
104 })
105}
106
107pub(crate) fn required_refs(
108 kind: &'static str,
109 id: EntityId,
110 entity: &Entity,
111 slot: usize,
112 attribute: &'static str,
113) -> ConstraintResult<Vec<EntityId>> {
114 let value = entity
115 .attribute(slot)
116 .ok_or(ConstraintError::MissingAttribute {
117 entity: kind,
118 id,
119 attribute,
120 })?;
121 let Value::List(values) = value else {
122 return Err(invalid(kind, id, attribute, value));
123 };
124 if values.is_empty() {
125 return Err(invalid(kind, id, attribute, value));
126 }
127 let mut seen = HashSet::new();
128 let mut out = Vec::with_capacity(values.len());
129 for item in values {
130 let Value::Ref(target) = item else {
131 return Err(invalid(kind, id, attribute, item));
132 };
133 if !seen.insert(*target) {
134 return Err(ConstraintError::InvalidValue {
135 entity: kind,
136 id,
137 attribute,
138 value: format!("duplicate {target}"),
139 });
140 }
141 out.push(*target);
142 }
143 Ok(out)
144}
145
146pub(crate) fn validate_target(
147 model: &Model,
148 kind: &'static str,
149 id: EntityId,
150 attribute: &'static str,
151 target: EntityId,
152 expected: &'static str,
153) -> ConstraintResult<()> {
154 let actual = model
155 .get(target)
156 .ok_or(ConstraintError::DanglingReference {
157 entity: kind,
158 id,
159 attribute,
160 target,
161 })?;
162 if ifc_schema::ifc4().accepts_type(expected, &actual.type_name) {
163 Ok(())
164 } else {
165 Err(ConstraintError::ReferenceType {
166 entity: kind,
167 id,
168 attribute,
169 target,
170 expected,
171 actual: actual.type_name.to_string(),
172 })
173 }
174}