1use std::collections::HashMap;
4
5use crate::kinds::Kind;
6
7#[derive(Debug, Clone)]
9pub struct Slot {
10 pub name: String,
12 pub type_ref: Option<String>,
14 pub meta: HashMap<String, Kind>,
16 pub default: Option<Kind>,
18 pub is_marker: bool,
20 pub is_query: bool,
22 pub children: Vec<Slot>,
24}
25
26impl Slot {
27 pub fn is_maybe(&self) -> bool {
29 self.meta.contains_key("maybe")
30 }
31}
32
33#[derive(Debug, Clone)]
35pub struct Spec {
36 pub qname: String,
38 pub name: String,
40 pub lib: String,
42 pub base: Option<String>,
44 pub meta: HashMap<String, Kind>,
46 pub slots: Vec<Slot>,
48 pub is_abstract: bool,
50 pub doc: String,
52}
53
54impl Spec {
55 pub fn new(qname: impl Into<String>, lib: impl Into<String>, name: impl Into<String>) -> Self {
57 Self {
58 qname: qname.into(),
59 name: name.into(),
60 lib: lib.into(),
61 base: None,
62 meta: HashMap::new(),
63 slots: Vec::new(),
64 is_abstract: false,
65 doc: String::new(),
66 }
67 }
68
69 pub fn markers(&self) -> Vec<&str> {
71 self.slots
72 .iter()
73 .filter(|s| s.is_marker)
74 .map(|s| s.name.as_str())
75 .collect()
76 }
77
78 pub fn mandatory_markers(&self) -> Vec<&str> {
80 self.slots
81 .iter()
82 .filter(|s| s.is_marker && !s.is_maybe())
83 .map(|s| s.name.as_str())
84 .collect()
85 }
86
87 pub fn effective_slots(&self, specs: &HashMap<String, Spec>) -> Vec<Slot> {
89 let mut visited = std::collections::HashSet::new();
90 self.collect_effective_slots(specs, &mut visited)
91 }
92
93 fn collect_effective_slots(
94 &self,
95 specs: &HashMap<String, Spec>,
96 visited: &mut std::collections::HashSet<String>,
97 ) -> Vec<Slot> {
98 if !visited.insert(self.qname.clone()) {
99 return Vec::new();
100 }
101 let mut inherited: Vec<Slot> = Vec::new();
102 if let Some(ref base_name) = self.base
103 && let Some(base_spec) = specs.get(base_name)
104 {
105 inherited = base_spec.collect_effective_slots(specs, visited);
106 }
107 let own_names: std::collections::HashSet<&str> =
108 self.slots.iter().map(|s| s.name.as_str()).collect();
109 let mut result: Vec<Slot> = inherited
110 .into_iter()
111 .filter(|s| !own_names.contains(s.name.as_str()))
112 .collect();
113 result.extend(self.slots.iter().cloned());
114 result
115 }
116
117 pub fn point_specs(&self) -> Vec<&Slot> {
119 self.slots
120 .iter()
121 .find(|s| s.name == "points")
122 .map(|s| s.children.iter().collect())
123 .unwrap_or_default()
124 }
125}
126
127impl From<&super::ast::SlotDef> for Slot {
129 fn from(def: &super::ast::SlotDef) -> Self {
130 Slot {
131 name: def.name.clone(),
132 type_ref: def.type_ref.clone(),
133 meta: def.meta.clone(),
134 default: def.default.clone(),
135 is_marker: def.is_marker,
136 is_query: def.is_query,
137 children: def.children.iter().map(Slot::from).collect(),
138 }
139 }
140}
141
142pub fn spec_from_def(def: &super::ast::SpecDef, lib_name: &str) -> Spec {
144 let qname = format!("{}::{}", lib_name, def.name);
145 let is_abstract = def.meta.contains_key("abstract");
146 Spec {
147 qname,
148 name: def.name.clone(),
149 lib: lib_name.to_string(),
150 base: def.base.clone(),
151 meta: def.meta.clone(),
152 slots: def.slots.iter().map(Slot::from).collect(),
153 is_abstract,
154 doc: def.doc.clone(),
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::xeto::ast::SlotDef;
162
163 fn make_marker_slot(name: &str) -> Slot {
164 Slot {
165 name: name.to_string(),
166 type_ref: None,
167 meta: HashMap::new(),
168 default: None,
169 is_marker: true,
170 is_query: false,
171 children: Vec::new(),
172 }
173 }
174
175 fn make_typed_slot(name: &str, type_ref: &str) -> Slot {
176 Slot {
177 name: name.to_string(),
178 type_ref: Some(type_ref.to_string()),
179 meta: HashMap::new(),
180 default: None,
181 is_marker: false,
182 is_query: false,
183 children: Vec::new(),
184 }
185 }
186
187 fn make_maybe_marker(name: &str) -> Slot {
188 let mut meta = HashMap::new();
189 meta.insert("maybe".to_string(), Kind::Marker);
190 Slot {
191 name: name.to_string(),
192 type_ref: None,
193 meta,
194 default: None,
195 is_marker: true,
196 is_query: false,
197 children: Vec::new(),
198 }
199 }
200
201 #[test]
202 fn spec_markers() {
203 let mut spec = Spec::new("test::Ahu", "test", "Ahu");
204 spec.slots.push(make_marker_slot("hot"));
205 spec.slots.push(make_marker_slot("cold"));
206 spec.slots.push(make_typed_slot("dis", "Str"));
207
208 let markers = spec.markers();
209 assert_eq!(markers, vec!["hot", "cold"]);
210 }
211
212 #[test]
213 fn spec_mandatory_markers() {
214 let mut spec = Spec::new("test::Ahu", "test", "Ahu");
215 spec.slots.push(make_marker_slot("hot"));
216 spec.slots.push(make_maybe_marker("cold"));
217
218 let mandatory = spec.mandatory_markers();
219 assert_eq!(mandatory, vec!["hot"]);
220 }
221
222 #[test]
223 fn spec_point_specs() {
224 let mut spec = Spec::new("test::Ahu", "test", "Ahu");
225 let points_slot = Slot {
226 name: "points".to_string(),
227 type_ref: None,
228 meta: HashMap::new(),
229 default: None,
230 is_marker: false,
231 is_query: true,
232 children: vec![
233 make_typed_slot("temp", "Point"),
234 make_typed_slot("flow", "Point"),
235 ],
236 };
237 spec.slots.push(points_slot);
238
239 let points = spec.point_specs();
240 assert_eq!(points.len(), 2);
241 assert_eq!(points[0].name, "temp");
242 assert_eq!(points[1].name, "flow");
243 }
244
245 #[test]
246 fn spec_point_specs_no_points_slot() {
247 let spec = Spec::new("test::Ahu", "test", "Ahu");
248 assert!(spec.point_specs().is_empty());
249 }
250
251 #[test]
252 fn slot_is_maybe() {
253 let mut slot = make_marker_slot("optional");
254 assert!(!slot.is_maybe());
255 slot.meta.insert("maybe".to_string(), Kind::Marker);
256 assert!(slot.is_maybe());
257 }
258
259 #[test]
260 fn slot_from_ast_slot_def() {
261 let mut def = SlotDef::new("dis");
262 def.type_ref = Some("Str".to_string());
263 def.is_marker = false;
264 let slot = Slot::from(&def);
265 assert_eq!(slot.name, "dis");
266 assert_eq!(slot.type_ref.as_deref(), Some("Str"));
267 }
268
269 #[test]
270 fn spec_from_def_conversion() {
271 let mut def = crate::xeto::ast::SpecDef::new("Ahu");
272 def.base = Some("Equip".to_string());
273 def.meta.insert("abstract".to_string(), Kind::Marker);
274 def.doc = "An AHU".to_string();
275
276 let spec = spec_from_def(&def, "phIoT");
277 assert_eq!(spec.qname, "phIoT::Ahu");
278 assert_eq!(spec.name, "Ahu");
279 assert_eq!(spec.lib, "phIoT");
280 assert_eq!(spec.base.as_deref(), Some("Equip"));
281 assert!(spec.is_abstract);
282 assert_eq!(spec.doc, "An AHU");
283 }
284
285 #[test]
286 fn spec_new() {
287 let spec = Spec::new("mylib::Foo", "mylib", "Foo");
288 assert_eq!(spec.qname, "mylib::Foo");
289 assert_eq!(spec.name, "Foo");
290 assert_eq!(spec.lib, "mylib");
291 assert!(spec.base.is_none());
292 assert!(!spec.is_abstract);
293 assert!(spec.slots.is_empty());
294 }
295
296 #[test]
297 fn effective_slots_cycle_does_not_stackoverflow() {
298 let mut spec_a = Spec::new("test::A", "test", "A");
300 spec_a.base = Some("test::B".to_string());
301 spec_a.slots.push(make_marker_slot("tagA"));
302
303 let mut spec_b = Spec::new("test::B", "test", "B");
304 spec_b.base = Some("test::A".to_string());
305 spec_b.slots.push(make_marker_slot("tagB"));
306
307 let mut specs = HashMap::new();
308 specs.insert("test::A".to_string(), spec_a.clone());
309 specs.insert("test::B".to_string(), spec_b.clone());
310
311 let slots_a = spec_a.effective_slots(&specs);
314 let names_a: Vec<&str> = slots_a.iter().map(|s| s.name.as_str()).collect();
315 assert!(names_a.contains(&"tagA"), "A should have its own tagA");
316
317 let slots_b = spec_b.effective_slots(&specs);
318 let names_b: Vec<&str> = slots_b.iter().map(|s| s.name.as_str()).collect();
319 assert!(names_b.contains(&"tagB"), "B should have its own tagB");
320 }
321
322 #[test]
323 fn effective_slots_includes_inherited() {
324 let mut parent = Spec::new("test::Parent", "test", "Parent");
325 parent.slots.push(make_marker_slot("equip"));
326 parent.slots.push(make_typed_slot("dis", "Str"));
327
328 let mut child = Spec::new("test::Child", "test", "Child");
329 child.base = Some("test::Parent".to_string());
330 child.slots.push(make_marker_slot("ahu"));
331 child.slots.push(make_typed_slot("dis", "Str"));
333
334 let mut specs = HashMap::new();
335 specs.insert("test::Parent".to_string(), parent);
336 specs.insert("test::Child".to_string(), child.clone());
337
338 let effective = child.effective_slots(&specs);
339 let names: Vec<&str> = effective.iter().map(|s| s.name.as_str()).collect();
340
341 assert!(names.contains(&"equip"), "should inherit equip from parent");
343 assert!(names.contains(&"ahu"), "should have own ahu");
344 assert!(names.contains(&"dis"), "should have dis");
345 let dis_count = names.iter().filter(|n| **n == "dis").count();
347 assert_eq!(dis_count, 1, "dis should not be duplicated");
348 }
349}