Skip to main content

ifc_constraint/
view.rs

1//! Shared borrowed model view and strict positional decoders.
2
3use std::collections::HashSet;
4
5use ifc_model::{Entity, EntityId, Model, Value};
6
7use crate::{ConstraintError, ConstraintResult};
8
9/// Borrowed entry point for constraint projections and inverse-style queries.
10#[derive(Debug, Clone, Copy)]
11pub struct ConstraintView<'m> {
12    model: &'m Model,
13}
14
15impl<'m> ConstraintView<'m> {
16    /// Borrow constraint semantics from a model snapshot.
17    #[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}