1use crate::item::CostItem;
8use crate::schedule::CostSchedule;
9use crate::value::CostValue;
10use ifc_model::Model;
11
12#[derive(Debug, Clone, Copy)]
14pub struct CostView<'m> {
15 model: &'m Model,
16}
17
18impl<'m> CostView<'m> {
19 pub fn new(model: &'m Model) -> Self {
21 Self { model }
22 }
23
24 pub fn schedules(&self) -> impl Iterator<Item = CostSchedule<'m>> + '_ {
26 self.model
27 .of_type("IFCCOSTSCHEDULE")
28 .map(|(id, entity)| CostSchedule::new(id, entity))
29 .collect::<Vec<_>>()
30 .into_iter()
31 }
32
33 pub fn items(&self) -> impl Iterator<Item = CostItem<'m>> + '_ {
35 self.model
36 .of_type("IFCCOSTITEM")
37 .map(|(id, entity)| CostItem::new(id, entity))
38 .collect::<Vec<_>>()
39 .into_iter()
40 }
41
42 pub fn model(&self) -> &'m Model {
44 self.model
45 }
46
47 pub fn values_of(&self, item: &CostItem<'m>) -> Vec<CostValue<'m>> {
54 item.value_refs()
55 .into_iter()
56 .filter_map(|id| self.model.get(id).map(|e| CostValue::new(id, e)))
57 .collect()
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use ifc_model::{Entity, EntityId, Value};
65
66 fn model_with_cost() -> Model {
67 let mut model = Model::new();
68 model.insert(
69 EntityId(1),
70 Entity::new(
71 "IFCCOSTVALUE",
72 vec![
73 Value::Text("Estimate".into()),
74 Value::Null,
75 Value::Typed {
76 type_name: "IFCMONETARYMEASURE".into(),
77 value: Box::new(Value::Real(1500.50)),
78 },
79 ],
80 ),
81 );
82 model.insert(
83 EntityId(2),
84 Entity::new(
85 "IFCCOSTITEM",
86 vec![
87 Value::Text("3vB2Y0dTv1LhX9ZzQqFbcd".into()), Value::Null, Value::Text("Excavation".into()), Value::Null, Value::Null, Value::Text("A.1".into()), Value::Enum("MATERIAL".into()), Value::List(vec![Value::Ref(EntityId(1))]), ],
96 ),
97 );
98 model
99 }
100
101 #[test]
102 fn finds_cost_items_without_the_model_knowing_what_cost_is() {
103 let model = model_with_cost();
104 let view = CostView::new(&model);
105 let items: Vec<_> = view.items().collect();
106 assert_eq!(items.len(), 1);
107 assert_eq!(items[0].name(), Some("Excavation"));
108 }
109
110 #[test]
111 fn resolves_values_through_references() {
112 let model = model_with_cost();
113 let view = CostView::new(&model);
114 let item = view.items().next().unwrap();
115 let values = view.values_of(&item);
116 assert_eq!(values.len(), 1);
117 assert_eq!(values[0].amount(), Some(1500.50));
118 }
119
120 #[test]
122 fn view_owns_nothing_and_model_is_unchanged() {
123 let model = model_with_cost();
124 let before = model.len();
125 {
126 let view = CostView::new(&model);
127 let _ = view.items().count();
128 }
129 assert_eq!(model.len(), before);
130 }
131
132 #[test]
140 fn cost_item_slots_match_the_inheritance_chain() {
141 let model = model_with_cost();
142 let view = CostView::new(&model);
143 let item = view
144 .items()
145 .find(|i| i.name() == Some("Excavation"))
146 .expect("item present");
147
148 assert_eq!(
149 item.identification(),
150 Some("A.1"),
151 "Identification is slot 5"
152 );
153 assert_eq!(
154 item.predefined_type(),
155 Some("MATERIAL"),
156 "PredefinedType is slot 6"
157 );
158 assert_eq!(item.value_refs(), vec![EntityId(1)], "CostValues is slot 7");
159 }
160
161 #[test]
163 fn unresolvable_value_references_are_skipped() {
164 let mut model = model_with_cost();
165 model.insert(
166 EntityId(3),
167 Entity::new(
168 "IFCCOSTITEM",
169 vec![
170 Value::Text("broken".into()),
171 Value::Null,
172 Value::Text("Dangling".into()),
173 Value::Null,
174 Value::Null,
175 Value::Null,
176 Value::Null,
177 Value::List(vec![Value::Ref(EntityId(999))]), ],
179 ),
180 );
181
182 let view = CostView::new(&model);
183 let item = view
184 .items()
185 .find(|i| i.name() == Some("Dangling"))
186 .expect("item present");
187 assert!(view.values_of(&item).is_empty(), "missing target skipped");
188 }
189}