1use crate::metamodel::{Aspect, CharacteristicKind, Entity, ModelElement, Operation, Property};
37use std::collections::{HashMap, HashSet, VecDeque};
38
39pub struct ModelQuery<'a> {
43 aspect: &'a Aspect,
44}
45
46#[derive(Debug, Clone, PartialEq)]
50pub struct ComplexityMetrics {
51 pub total_properties: usize,
53 pub total_entities: usize,
55 pub total_operations: usize,
57 pub max_nesting_depth: usize,
59 pub optional_properties: usize,
61 pub collection_properties: usize,
63 pub circular_references: usize,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct Dependency {
70 pub from: String,
72 pub to: String,
74 pub dependency_type: String,
76}
77
78impl<'a> ModelQuery<'a> {
79 pub fn new(aspect: &'a Aspect) -> Self {
91 Self { aspect }
92 }
93
94 pub fn find_properties_with_collection_characteristic(&self) -> Vec<&Property> {
100 self.aspect
101 .properties()
102 .iter()
103 .filter(|prop| {
104 if let Some(ref characteristic) = prop.characteristic {
105 matches!(
106 characteristic.kind(),
107 CharacteristicKind::Collection { .. }
108 | CharacteristicKind::List { .. }
109 | CharacteristicKind::Set { .. }
110 | CharacteristicKind::SortedSet { .. }
111 )
112 } else {
113 false
114 }
115 })
116 .collect()
117 }
118
119 pub fn find_optional_properties(&self) -> Vec<&Property> {
125 self.aspect
126 .properties()
127 .iter()
128 .filter(|prop| prop.optional)
129 .collect()
130 }
131
132 pub fn find_required_properties(&self) -> Vec<&Property> {
138 self.aspect
139 .properties()
140 .iter()
141 .filter(|prop| !prop.optional)
142 .collect()
143 }
144
145 pub fn find_properties_in_namespace(&self, namespace: &str) -> Vec<&Property> {
155 self.aspect
156 .properties()
157 .iter()
158 .filter(|prop| {
159 if let Some(prop_ns) = prop.urn().rsplit_once('#').map(|(ns, _)| ns) {
161 if let Some(target_ns) = namespace.rsplit_once('#').map(|(ns, _)| ns) {
162 prop_ns == target_ns
163 } else {
164 prop_ns == namespace
165 }
166 } else {
167 false
168 }
169 })
170 .collect()
171 }
172
173 pub fn find_properties_by_characteristic<F>(&self, predicate: F) -> Vec<&Property>
183 where
184 F: Fn(&CharacteristicKind) -> bool,
185 {
186 self.aspect
187 .properties()
188 .iter()
189 .filter(|prop| {
190 if let Some(ref characteristic) = prop.characteristic {
191 predicate(characteristic.kind())
192 } else {
193 false
194 }
195 })
196 .collect()
197 }
198
199 pub fn find_all_referenced_entities(&self) -> HashSet<String> {
208 let mut entities = HashSet::new();
209 let mut visited = HashSet::new();
210 let mut queue = VecDeque::new();
211
212 for property in self.aspect.properties() {
214 if !visited.contains(property.urn()) {
215 queue.push_back(property);
216 visited.insert(property.urn().to_string());
217 }
218 }
219
220 while let Some(property) = queue.pop_front() {
222 if let Some(ref characteristic) = property.characteristic {
223 match characteristic.kind() {
225 CharacteristicKind::SingleEntity { .. } => {
226 if let Some(ref data_type) = characteristic.data_type {
227 entities.insert(data_type.clone());
228 }
229 }
230 CharacteristicKind::Collection {
231 element_characteristic,
232 ..
233 }
234 | CharacteristicKind::List {
235 element_characteristic,
236 ..
237 }
238 | CharacteristicKind::Set {
239 element_characteristic,
240 ..
241 }
242 | CharacteristicKind::SortedSet {
243 element_characteristic,
244 ..
245 } => {
246 if let Some(elem_char) = element_characteristic {
247 if let Some(ref data_type) = elem_char.data_type {
248 entities.insert(data_type.clone());
249 }
250 }
251 }
252 _ => {}
253 }
254 }
255 }
256
257 entities
258 }
259
260 pub fn build_dependency_graph(&self) -> Vec<Dependency> {
266 let mut dependencies = Vec::new();
267
268 for property in self.aspect.properties() {
270 if let Some(ref characteristic) = property.characteristic {
271 dependencies.push(Dependency {
272 from: property.urn().to_string(),
273 to: characteristic.urn().to_string(),
274 dependency_type: "characteristic".to_string(),
275 });
276
277 if let Some(ref data_type) = characteristic.data_type {
279 dependencies.push(Dependency {
280 from: characteristic.urn().to_string(),
281 to: data_type.clone(),
282 dependency_type: "datatype".to_string(),
283 });
284 }
285
286 match characteristic.kind() {
288 CharacteristicKind::Collection {
289 element_characteristic,
290 ..
291 }
292 | CharacteristicKind::List {
293 element_characteristic,
294 ..
295 }
296 | CharacteristicKind::Set {
297 element_characteristic,
298 ..
299 }
300 | CharacteristicKind::SortedSet {
301 element_characteristic,
302 ..
303 } => {
304 if let Some(elem_char) = element_characteristic {
305 dependencies.push(Dependency {
306 from: characteristic.urn().to_string(),
307 to: elem_char.urn().to_string(),
308 dependency_type: "element_characteristic".to_string(),
309 });
310 }
311 }
312 _ => {}
313 }
314 }
315 }
316
317 for operation in self.aspect.operations() {
319 for input in operation.input() {
321 dependencies.push(Dependency {
322 from: operation.urn().to_string(),
323 to: input.urn().to_string(),
324 dependency_type: "input".to_string(),
325 });
326 }
327
328 if let Some(output) = operation.output() {
330 dependencies.push(Dependency {
331 from: operation.urn().to_string(),
332 to: output.urn().to_string(),
333 dependency_type: "output".to_string(),
334 });
335 }
336 }
337
338 dependencies
339 }
340
341 pub fn detect_circular_dependencies(&self) -> Vec<Vec<String>> {
349 let dependencies = self.build_dependency_graph();
350 let mut graph: HashMap<String, Vec<String>> = HashMap::new();
351
352 for dep in &dependencies {
354 graph
355 .entry(dep.from.clone())
356 .or_default()
357 .push(dep.to.clone());
358 }
359
360 let mut cycles = Vec::new();
361 let mut visited = HashSet::new();
362 let mut rec_stack = HashSet::new();
363 let mut path = Vec::new();
364
365 for node in graph.keys() {
366 if !visited.contains(node) {
367 Self::dfs_detect_cycle(
368 node,
369 &graph,
370 &mut visited,
371 &mut rec_stack,
372 &mut path,
373 &mut cycles,
374 );
375 }
376 }
377
378 cycles
379 }
380
381 fn dfs_detect_cycle(
383 node: &str,
384 graph: &HashMap<String, Vec<String>>,
385 visited: &mut HashSet<String>,
386 rec_stack: &mut HashSet<String>,
387 path: &mut Vec<String>,
388 cycles: &mut Vec<Vec<String>>,
389 ) {
390 visited.insert(node.to_string());
391 rec_stack.insert(node.to_string());
392 path.push(node.to_string());
393
394 if let Some(neighbors) = graph.get(node) {
395 for neighbor in neighbors {
396 if !visited.contains(neighbor) {
397 Self::dfs_detect_cycle(neighbor, graph, visited, rec_stack, path, cycles);
398 } else if rec_stack.contains(neighbor) {
399 if let Some(cycle_start) = path.iter().position(|n| n == neighbor) {
401 cycles.push(path[cycle_start..].to_vec());
402 }
403 }
404 }
405 }
406
407 path.pop();
408 rec_stack.remove(node);
409 }
410
411 pub fn complexity_metrics(&self) -> ComplexityMetrics {
417 let total_properties = self.aspect.properties().len();
418 let total_operations = self.aspect.operations().len();
419 let optional_properties = self.find_optional_properties().len();
420 let collection_properties = self.find_properties_with_collection_characteristic().len();
421 let total_entities = self.find_all_referenced_entities().len();
422 let circular_references = self.detect_circular_dependencies().len();
423
424 let max_nesting_depth = self.calculate_max_nesting_depth();
426
427 ComplexityMetrics {
428 total_properties,
429 total_entities,
430 total_operations,
431 max_nesting_depth,
432 optional_properties,
433 collection_properties,
434 circular_references,
435 }
436 }
437
438 fn calculate_max_nesting_depth(&self) -> usize {
440 let mut max_depth = 1; for property in self.aspect.properties() {
443 if let Some(ref characteristic) = property.characteristic {
444 let depth =
445 Self::calculate_characteristic_depth(characteristic, &mut HashSet::new());
446 max_depth = max_depth.max(depth);
447 }
448 }
449
450 max_depth
451 }
452
453 fn calculate_characteristic_depth(
455 characteristic: &crate::metamodel::Characteristic,
456 visited: &mut HashSet<String>,
457 ) -> usize {
458 if visited.contains(characteristic.urn()) {
459 return 0; }
461 visited.insert(characteristic.urn().to_string());
462
463 match characteristic.kind() {
464 CharacteristicKind::Collection {
465 element_characteristic,
466 ..
467 }
468 | CharacteristicKind::List {
469 element_characteristic,
470 ..
471 }
472 | CharacteristicKind::Set {
473 element_characteristic,
474 ..
475 }
476 | CharacteristicKind::SortedSet {
477 element_characteristic,
478 ..
479 } => {
480 if let Some(elem_char) = element_characteristic {
481 1 + Self::calculate_characteristic_depth(elem_char, visited)
482 } else {
483 1
484 }
485 }
486 CharacteristicKind::SingleEntity { .. } => 2,
487 _ => 1,
488 }
489 }
490
491 pub fn find_properties_by_name_pattern(&self, pattern: &str) -> Vec<&Property> {
501 let pattern_lower = pattern.to_lowercase();
502 self.aspect
503 .properties()
504 .iter()
505 .filter(|prop| prop.name().to_lowercase().contains(&pattern_lower))
506 .collect()
507 }
508
509 pub fn group_properties_by_characteristic_type(&self) -> HashMap<String, Vec<&Property>> {
515 let mut groups: HashMap<String, Vec<&Property>> = HashMap::new();
516
517 for property in self.aspect.properties() {
518 let type_name = if let Some(ref characteristic) = property.characteristic {
519 match characteristic.kind() {
520 CharacteristicKind::Trait => "Trait".to_string(),
521 CharacteristicKind::Quantifiable { .. } => "Quantifiable".to_string(),
522 CharacteristicKind::Measurement { .. } => "Measurement".to_string(),
523 CharacteristicKind::Enumeration { .. } => "Enumeration".to_string(),
524 CharacteristicKind::State { .. } => "State".to_string(),
525 CharacteristicKind::Duration { .. } => "Duration".to_string(),
526 CharacteristicKind::Collection { .. } => "Collection".to_string(),
527 CharacteristicKind::List { .. } => "List".to_string(),
528 CharacteristicKind::Set { .. } => "Set".to_string(),
529 CharacteristicKind::SortedSet { .. } => "SortedSet".to_string(),
530 CharacteristicKind::TimeSeries { .. } => "TimeSeries".to_string(),
531 CharacteristicKind::Code => "Code".to_string(),
532 CharacteristicKind::Either { .. } => "Either".to_string(),
533 CharacteristicKind::SingleEntity { .. } => "SingleEntity".to_string(),
534 CharacteristicKind::StructuredValue { .. } => "StructuredValue".to_string(),
535 }
536 } else {
537 "NoCharacteristic".to_string()
538 };
539
540 groups.entry(type_name).or_default().push(property);
541 }
542
543 groups
544 }
545
546 pub fn aspect(&self) -> &Aspect {
548 self.aspect
549 }
550
551 pub fn fuzzy_find_properties(
581 &self,
582 query: &str,
583 max_distance: usize,
584 ) -> Vec<(&Property, usize)> {
585 let mut results: Vec<(&Property, usize)> = self
586 .aspect
587 .properties()
588 .iter()
589 .map(|prop| {
590 let distance = levenshtein_distance(query, &prop.name());
591 (prop, distance)
592 })
593 .filter(|(_, distance)| *distance <= max_distance)
594 .collect();
595
596 results.sort_by_key(|(_, distance)| *distance);
598 results
599 }
600
601 pub fn fuzzy_find_operations(
614 &self,
615 query: &str,
616 max_distance: usize,
617 ) -> Vec<(&Operation, usize)> {
618 let mut results: Vec<(&Operation, usize)> = self
619 .aspect
620 .operations()
621 .iter()
622 .map(|op| {
623 let distance = levenshtein_distance(query, &op.name());
624 (op, distance)
625 })
626 .filter(|(_, distance)| *distance <= max_distance)
627 .collect();
628
629 results.sort_by_key(|(_, distance)| *distance);
630 results
631 }
632
633 pub fn fuzzy_find_any_element(
646 &self,
647 query: &str,
648 max_distance: usize,
649 ) -> Vec<(String, String, usize)> {
650 let mut results = Vec::new();
651
652 for prop in self.aspect.properties() {
654 let distance = levenshtein_distance(query, &prop.name());
655 if distance <= max_distance {
656 results.push((prop.name().to_string(), prop.urn().to_string(), distance));
657 }
658 }
659
660 for op in self.aspect.operations() {
662 let distance = levenshtein_distance(query, &op.name());
663 if distance <= max_distance {
664 results.push((op.name().to_string(), op.urn().to_string(), distance));
665 }
666 }
667
668 results.sort_by_key(|(_, _, distance)| *distance);
670 results
671 }
672
673 pub fn suggest_properties(&self, prefix: &str, limit: usize) -> Vec<String> {
687 let prefix_lower = prefix.to_lowercase();
688 let mut suggestions = Vec::new();
689
690 let mut prefix_matches: Vec<_> = self
692 .aspect
693 .properties()
694 .iter()
695 .filter(|prop| prop.name().to_lowercase().starts_with(&prefix_lower))
696 .map(|prop| (prop.name().to_string(), 0))
697 .collect();
698
699 let prefix_match_names: HashSet<_> = prefix_matches
701 .iter()
702 .map(|(name, _)| name.clone())
703 .collect();
704
705 let mut fuzzy_matches: Vec<_> = self
706 .aspect
707 .properties()
708 .iter()
709 .filter(|prop| !prefix_match_names.contains(&prop.name()))
710 .map(|prop| {
711 let distance = levenshtein_distance(prefix, &prop.name());
712 (prop.name().to_string(), distance)
713 })
714 .filter(|(_, distance)| *distance <= 3)
715 .collect();
716
717 suggestions.append(&mut prefix_matches);
719 suggestions.append(&mut fuzzy_matches);
720 suggestions.sort_by_key(|(_, distance)| *distance);
721 suggestions.truncate(limit);
722
723 suggestions.into_iter().map(|(name, _)| name).collect()
724 }
725}
726
727#[allow(clippy::needless_range_loop)]
741fn levenshtein_distance(a: &str, b: &str) -> usize {
742 let a_len = a.chars().count();
743 let b_len = b.chars().count();
744
745 if a_len == 0 {
746 return b_len;
747 }
748 if b_len == 0 {
749 return a_len;
750 }
751
752 let mut matrix = vec![vec![0; b_len + 1]; a_len + 1];
753
754 for i in 0..=a_len {
756 matrix[i][0] = i;
757 }
758 for j in 0..=b_len {
759 matrix[0][j] = j;
760 }
761
762 let a_chars: Vec<char> = a.chars().collect();
764 let b_chars: Vec<char> = b.chars().collect();
765
766 for (i, a_char) in a_chars.iter().enumerate() {
767 for (j, b_char) in b_chars.iter().enumerate() {
768 let cost = if a_char == b_char { 0 } else { 1 };
769
770 matrix[i + 1][j + 1] = *[
771 matrix[i][j + 1] + 1, matrix[i + 1][j] + 1, matrix[i][j] + cost, ]
775 .iter()
776 .min()
777 .expect("operation should succeed");
778 }
779 }
780
781 matrix[a_len][b_len]
782}
783
784#[cfg(test)]
785mod tests {
786 use super::*;
787 use crate::metamodel::{Characteristic, CharacteristicKind};
788
789 #[test]
790 fn test_find_optional_properties() {
791 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
792
793 let prop1 = Property::new("urn:samm:test:1.0.0#required".to_string());
794
795 let mut prop2 = Property::new("urn:samm:test:1.0.0#optional".to_string());
796 prop2.optional = true;
797
798 aspect.add_property(prop1);
799 aspect.add_property(prop2);
800
801 let query = ModelQuery::new(&aspect);
802 let optional = query.find_optional_properties();
803
804 assert_eq!(optional.len(), 1);
805 assert_eq!(optional[0].name(), "optional");
806 }
807
808 #[test]
809 fn test_find_required_properties() {
810 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
811
812 let prop1 = Property::new("urn:samm:test:1.0.0#required".to_string());
813
814 let mut prop2 = Property::new("urn:samm:test:1.0.0#optional".to_string());
815 prop2.optional = true;
816
817 aspect.add_property(prop1);
818 aspect.add_property(prop2);
819
820 let query = ModelQuery::new(&aspect);
821 let required = query.find_required_properties();
822
823 assert_eq!(required.len(), 1);
824 assert_eq!(required[0].name(), "required");
825 }
826
827 #[test]
828 fn test_find_collection_properties() {
829 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
830
831 let mut prop1 = Property::new("urn:samm:test:1.0.0#list".to_string());
832 let char1 = Characteristic::new(
833 "urn:samm:test:1.0.0#ListChar".to_string(),
834 CharacteristicKind::List {
835 element_characteristic: None,
836 },
837 );
838 prop1.characteristic = Some(char1);
839
840 let mut prop2 = Property::new("urn:samm:test:1.0.0#simple".to_string());
841 let char2 = Characteristic::new(
842 "urn:samm:test:1.0.0#TraitChar".to_string(),
843 CharacteristicKind::Trait,
844 );
845 prop2.characteristic = Some(char2);
846
847 aspect.add_property(prop1);
848 aspect.add_property(prop2);
849
850 let query = ModelQuery::new(&aspect);
851 let collections = query.find_properties_with_collection_characteristic();
852
853 assert_eq!(collections.len(), 1);
854 assert_eq!(collections[0].name(), "list");
855 }
856
857 #[test]
858 fn test_complexity_metrics() {
859 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
860
861 let prop1 = Property::new("urn:samm:test:1.0.0#prop1".to_string());
862
863 let mut prop2 = Property::new("urn:samm:test:1.0.0#prop2".to_string());
864 prop2.optional = true;
865
866 aspect.add_property(prop1);
867 aspect.add_property(prop2);
868
869 let query = ModelQuery::new(&aspect);
870 let metrics = query.complexity_metrics();
871
872 assert_eq!(metrics.total_properties, 2);
873 assert_eq!(metrics.optional_properties, 1);
874 assert_eq!(metrics.total_operations, 0);
875 }
876
877 #[test]
878 fn test_find_properties_by_name_pattern() {
879 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
880
881 aspect.add_property(Property::new("urn:samm:test:1.0.0#speedLimit".to_string()));
882 aspect.add_property(Property::new(
883 "urn:samm:test:1.0.0#currentSpeed".to_string(),
884 ));
885 aspect.add_property(Property::new("urn:samm:test:1.0.0#temperature".to_string()));
886
887 let query = ModelQuery::new(&aspect);
888 let results = query.find_properties_by_name_pattern("speed");
889
890 assert_eq!(results.len(), 2);
891 assert!(results.iter().any(|p| p.name() == "speedLimit"));
892 assert!(results.iter().any(|p| p.name() == "currentSpeed"));
893 }
894
895 #[test]
896 fn test_group_properties_by_characteristic_type() {
897 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
898
899 let mut prop1 = Property::new("urn:samm:test:1.0.0#list1".to_string());
900 prop1.characteristic = Some(Characteristic::new(
901 "urn:samm:test:1.0.0#ListChar1".to_string(),
902 CharacteristicKind::List {
903 element_characteristic: None,
904 },
905 ));
906
907 let mut prop2 = Property::new("urn:samm:test:1.0.0#list2".to_string());
908 prop2.characteristic = Some(Characteristic::new(
909 "urn:samm:test:1.0.0#ListChar2".to_string(),
910 CharacteristicKind::List {
911 element_characteristic: None,
912 },
913 ));
914
915 let mut prop3 = Property::new("urn:samm:test:1.0.0#trait1".to_string());
916 prop3.characteristic = Some(Characteristic::new(
917 "urn:samm:test:1.0.0#TraitChar".to_string(),
918 CharacteristicKind::Trait,
919 ));
920
921 aspect.add_property(prop1);
922 aspect.add_property(prop2);
923 aspect.add_property(prop3);
924
925 let query = ModelQuery::new(&aspect);
926 let groups = query.group_properties_by_characteristic_type();
927
928 assert_eq!(groups.get("List").expect("key should exist").len(), 2);
929 assert_eq!(groups.get("Trait").expect("key should exist").len(), 1);
930 }
931
932 #[test]
933 fn test_build_dependency_graph() {
934 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
935
936 let mut prop1 = Property::new("urn:samm:test:1.0.0#prop1".to_string());
937 let char1 = Characteristic::new(
938 "urn:samm:test:1.0.0#Char1".to_string(),
939 CharacteristicKind::Trait,
940 );
941 prop1.characteristic = Some(char1);
942
943 aspect.add_property(prop1);
944
945 let query = ModelQuery::new(&aspect);
946 let deps = query.build_dependency_graph();
947
948 assert!(!deps.is_empty());
949 assert!(deps.iter().any(|d| d.from.contains("prop1")));
950 assert!(deps.iter().any(|d| d.to.contains("Char1")));
951 }
952
953 #[test]
954 fn test_detect_circular_dependencies_none() {
955 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
956
957 let mut prop1 = Property::new("urn:samm:test:1.0.0#prop1".to_string());
958 let char1 = Characteristic::new(
959 "urn:samm:test:1.0.0#Char1".to_string(),
960 CharacteristicKind::Trait,
961 );
962 prop1.characteristic = Some(char1);
963
964 aspect.add_property(prop1);
965
966 let query = ModelQuery::new(&aspect);
967 let cycles = query.detect_circular_dependencies();
968
969 assert_eq!(cycles.len(), 0);
970 }
971
972 #[test]
973 fn test_find_properties_in_namespace() {
974 let mut aspect = Aspect::new("urn:samm:org.example:1.0.0#TestAspect".to_string());
975
976 aspect.add_property(Property::new(
977 "urn:samm:org.example:1.0.0#prop1".to_string(),
978 ));
979 aspect.add_property(Property::new("urn:samm:org.other:1.0.0#prop2".to_string()));
980
981 let query = ModelQuery::new(&aspect);
982 let results = query.find_properties_in_namespace("urn:samm:org.example:1.0.0");
983
984 assert_eq!(results.len(), 1);
985 assert_eq!(results[0].name(), "prop1");
986 }
987
988 #[test]
991 fn test_levenshtein_distance() {
992 assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
993 assert_eq!(levenshtein_distance("hello", "hello"), 0);
994 assert_eq!(levenshtein_distance("", "test"), 4);
995 assert_eq!(levenshtein_distance("test", ""), 4);
996 assert_eq!(levenshtein_distance("abc", "def"), 3);
997 }
998
999 #[test]
1000 fn test_fuzzy_find_properties_exact_match() {
1001 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
1002 aspect.add_property(Property::new("urn:samm:test:1.0.0#temperature".to_string()));
1003 aspect.add_property(Property::new("urn:samm:test:1.0.0#humidity".to_string()));
1004 aspect.add_property(Property::new("urn:samm:test:1.0.0#pressure".to_string()));
1005
1006 let query = ModelQuery::new(&aspect);
1007 let results = query.fuzzy_find_properties("temperature", 0);
1008
1009 assert_eq!(results.len(), 1);
1010 assert_eq!(results[0].0.name(), "temperature");
1011 assert_eq!(results[0].1, 0); }
1013
1014 #[test]
1015 fn test_fuzzy_find_properties_typo() {
1016 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
1017 aspect.add_property(Property::new("urn:samm:test:1.0.0#temperature".to_string()));
1018 aspect.add_property(Property::new("urn:samm:test:1.0.0#humidity".to_string()));
1019
1020 let query = ModelQuery::new(&aspect);
1021 let results = query.fuzzy_find_properties("temperture", 2);
1023
1024 assert!(!results.is_empty());
1025 assert!(results.iter().any(|(prop, _)| prop.name() == "temperature"));
1026 }
1027
1028 #[test]
1029 fn test_fuzzy_find_properties_sorted_by_distance() {
1030 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
1031 aspect.add_property(Property::new("urn:samm:test:1.0.0#temp".to_string()));
1032 aspect.add_property(Property::new("urn:samm:test:1.0.0#temperature".to_string()));
1033 aspect.add_property(Property::new("urn:samm:test:1.0.0#tempValue".to_string()));
1034
1035 let query = ModelQuery::new(&aspect);
1036 let results = query.fuzzy_find_properties("temp", 5);
1037
1038 assert!(!results.is_empty());
1040 assert_eq!(results[0].0.name(), "temp"); assert_eq!(results[0].1, 0);
1042 }
1043
1044 #[test]
1045 fn test_fuzzy_find_operations() {
1046 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
1047 aspect.add_operation(Operation::new(
1048 "urn:samm:test:1.0.0#startEngine".to_string(),
1049 ));
1050 aspect.add_operation(Operation::new("urn:samm:test:1.0.0#stopEngine".to_string()));
1051
1052 let query = ModelQuery::new(&aspect);
1053 let results = query.fuzzy_find_operations("startEngin", 2); assert!(!results.is_empty());
1056 assert!(results.iter().any(|(op, _)| op.name() == "startEngine"));
1057 }
1058
1059 #[test]
1060 fn test_fuzzy_find_any_element() {
1061 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
1062 aspect.add_property(Property::new("urn:samm:test:1.0.0#temperature".to_string()));
1063 aspect.add_operation(Operation::new("urn:samm:test:1.0.0#measure".to_string()));
1064
1065 let query = ModelQuery::new(&aspect);
1066 let results = query.fuzzy_find_any_element("temp", 8);
1068
1069 assert!(!results.is_empty());
1070 assert!(results.iter().any(|(name, _, _)| name == "temperature"));
1071 }
1072
1073 #[test]
1074 fn test_suggest_properties_prefix_match() {
1075 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
1076 aspect.add_property(Property::new("urn:samm:test:1.0.0#temperature".to_string()));
1077 aspect.add_property(Property::new("urn:samm:test:1.0.0#tempValue".to_string()));
1078 aspect.add_property(Property::new("urn:samm:test:1.0.0#humidity".to_string()));
1079
1080 let query = ModelQuery::new(&aspect);
1081 let suggestions = query.suggest_properties("temp", 5);
1082
1083 assert_eq!(suggestions.len(), 2);
1084 assert!(suggestions.contains(&"temperature".to_string()));
1085 assert!(suggestions.contains(&"tempValue".to_string()));
1086 }
1087
1088 #[test]
1089 fn test_suggest_properties_limit() {
1090 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
1091 aspect.add_property(Property::new("urn:samm:test:1.0.0#prop1".to_string()));
1092 aspect.add_property(Property::new("urn:samm:test:1.0.0#prop2".to_string()));
1093 aspect.add_property(Property::new("urn:samm:test:1.0.0#prop3".to_string()));
1094
1095 let query = ModelQuery::new(&aspect);
1096 let suggestions = query.suggest_properties("prop", 2);
1097
1098 assert_eq!(suggestions.len(), 2);
1099 }
1100
1101 #[test]
1102 fn test_suggest_properties_fuzzy_fallback() {
1103 let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
1104 aspect.add_property(Property::new("urn:samm:test:1.0.0#temperature".to_string()));
1105 aspect.add_property(Property::new("urn:samm:test:1.0.0#humidity".to_string()));
1106
1107 let query = ModelQuery::new(&aspect);
1108 let suggestions = query.suggest_properties("temper", 5);
1110
1111 assert!(!suggestions.is_empty());
1112 assert!(suggestions.contains(&"temperature".to_string()));
1113 }
1114}