Skip to main content

ifc_approval/
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::{ApprovalError, ApprovalResult};
8
9/// Borrowed entry point for approval projections and inverse-style queries.
10#[derive(Debug, Clone, Copy)]
11pub struct ApprovalView<'m> {
12    model: &'m Model,
13}
14
15impl<'m> ApprovalView<'m> {
16    /// Borrow approval 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) -> ApprovalError {
28    ApprovalError::WrongEntityType {
29        expected,
30        actual: entity.type_name.to_string(),
31    }
32}
33
34fn invalid(
35    kind: &'static str,
36    id: EntityId,
37    attribute: &'static str,
38    value: &Value,
39) -> ApprovalError {
40    ApprovalError::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) -> ApprovalResult<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) -> ApprovalResult<&'m str> {
72    optional_text(kind, id, entity, slot, attribute)?.ok_or(ApprovalError::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) -> ApprovalResult<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) -> ApprovalResult<EntityId> {
100    optional_ref(kind, id, entity, slot, attribute)?.ok_or(ApprovalError::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) -> ApprovalResult<Vec<EntityId>> {
114    let value = entity
115        .attribute(slot)
116        .ok_or(ApprovalError::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(ApprovalError::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) -> ApprovalResult<()> {
154    let actual = model.get(target).ok_or(ApprovalError::DanglingReference {
155        entity: kind,
156        id,
157        attribute,
158        target,
159    })?;
160    if ifc_schema::ifc4().accepts_type(expected, &actual.type_name) {
161        Ok(())
162    } else {
163        Err(ApprovalError::ReferenceType {
164            entity: kind,
165            id,
166            attribute,
167            target,
168            expected,
169            actual: actual.type_name.to_string(),
170        })
171    }
172}