Skip to main content

oxirs_samm/
query.rs

1//! Model Query Utilities
2//!
3//! This module provides powerful utilities for querying and analyzing SAMM Aspect Models.
4//! It enables sophisticated model introspection, dependency analysis, and element discovery.
5//!
6//! # Features
7//!
8//! - **Element Discovery**: Find properties, characteristics, entities, operations by criteria
9//! - **Dependency Analysis**: Discover element dependencies and circular references
10//! - **Type Queries**: Find all elements of specific types or with specific characteristics
11//! - **Naming Queries**: Search by name patterns, URN prefixes, or namespaces
12//! - **Statistical Queries**: Get counts, distributions, and complexity metrics
13//!
14//! # Examples
15//!
16//! ```rust,ignore
17//! use oxirs_samm::query::ModelQuery;
18//! use oxirs_samm::metamodel::Aspect;
19//!
20//! # fn example(aspect: &Aspect) {
21//! let query = ModelQuery::new(aspect);
22//!
23//! // Find all properties with Collection characteristics
24//! let collections = query.find_properties_with_collection_characteristic();
25//!
26//! // Find all referenced entities
27//! let entities = query.find_all_referenced_entities();
28//!
29//! // Get model complexity metrics
30//! let metrics = query.complexity_metrics();
31//! println!("Total properties: {}", metrics.total_properties);
32//! println!("Max nesting depth: {}", metrics.max_nesting_depth);
33//! # }
34//! ```
35
36use crate::metamodel::{Aspect, CharacteristicKind, Entity, ModelElement, Operation, Property};
37use std::collections::{HashMap, HashSet, VecDeque};
38
39/// Query builder for SAMM Aspect Models
40///
41/// Provides a fluent API for querying and analyzing SAMM models.
42pub struct ModelQuery<'a> {
43    aspect: &'a Aspect,
44}
45
46/// Complexity metrics for a SAMM model
47///
48/// Provides quantitative measures of model complexity.
49#[derive(Debug, Clone, PartialEq)]
50pub struct ComplexityMetrics {
51    /// Total number of properties
52    pub total_properties: usize,
53    /// Total number of entities
54    pub total_entities: usize,
55    /// Total number of operations
56    pub total_operations: usize,
57    /// Maximum nesting depth of entities
58    pub max_nesting_depth: usize,
59    /// Number of optional properties
60    pub optional_properties: usize,
61    /// Number of collection properties
62    pub collection_properties: usize,
63    /// Number of circular references detected
64    pub circular_references: usize,
65}
66
67/// Dependency information for a model element
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct Dependency {
70    /// URN of the source element
71    pub from: String,
72    /// URN of the target element
73    pub to: String,
74    /// Type of dependency (e.g., "property", "characteristic", "entity")
75    pub dependency_type: String,
76}
77
78impl<'a> ModelQuery<'a> {
79    /// Creates a new query builder for the given aspect
80    ///
81    /// # Examples
82    ///
83    /// ```rust,ignore
84    /// use oxirs_samm::query::ModelQuery;
85    /// # use oxirs_samm::metamodel::Aspect;
86    /// # fn example(aspect: &Aspect) {
87    /// let query = ModelQuery::new(aspect);
88    /// # }
89    /// ```
90    pub fn new(aspect: &'a Aspect) -> Self {
91        Self { aspect }
92    }
93
94    /// Finds all properties with Collection, List, Set, or SortedSet characteristics
95    ///
96    /// # Returns
97    ///
98    /// Vector of properties that have collection-type characteristics
99    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    /// Finds all optional properties
120    ///
121    /// # Returns
122    ///
123    /// Vector of properties marked as optional
124    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    /// Finds all required (non-optional) properties
133    ///
134    /// # Returns
135    ///
136    /// Vector of properties that are required
137    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    /// Finds properties by URN namespace
146    ///
147    /// # Arguments
148    ///
149    /// * `namespace` - The URN namespace to match (e.g., "urn:samm:org.example:1.0.0")
150    ///
151    /// # Returns
152    ///
153    /// Vector of properties in the specified namespace
154    pub fn find_properties_in_namespace(&self, namespace: &str) -> Vec<&Property> {
155        self.aspect
156            .properties()
157            .iter()
158            .filter(|prop| {
159                // Extract namespace from URN (before the #)
160                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    /// Finds all properties with specific characteristic type
174    ///
175    /// # Arguments
176    ///
177    /// * `predicate` - Function to test characteristic kind
178    ///
179    /// # Returns
180    ///
181    /// Vector of properties matching the predicate
182    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    /// Finds all entities referenced directly or indirectly by the aspect
200    ///
201    /// This performs a breadth-first search through all properties and their characteristics
202    /// to discover all entity references.
203    ///
204    /// # Returns
205    ///
206    /// Set of unique entity URNs referenced by the model
207    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        // Start with all properties
213        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        // BFS through properties and characteristics
221        while let Some(property) = queue.pop_front() {
222            if let Some(ref characteristic) = property.characteristic {
223                // Check for entity references in characteristic
224                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    /// Builds a dependency graph of all model elements
261    ///
262    /// # Returns
263    ///
264    /// Vector of dependencies showing relationships between elements
265    pub fn build_dependency_graph(&self) -> Vec<Dependency> {
266        let mut dependencies = Vec::new();
267
268        // Property -> Characteristic dependencies
269        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                // Characteristic -> DataType/Entity dependencies
278                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                // Handle nested characteristics (e.g., Collection element characteristic)
287                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        // Operation dependencies
318        for operation in self.aspect.operations() {
319            // Input property dependencies
320            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            // Output property dependencies
329            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    /// Detects circular dependencies in the model
342    ///
343    /// Uses depth-first search to detect cycles in the dependency graph.
344    ///
345    /// # Returns
346    ///
347    /// Vector of URN chains representing circular dependencies
348    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        // Build adjacency list
353        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    /// Helper function for DFS cycle detection
382    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                    // Found a cycle
400                    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    /// Calculates complexity metrics for the model
412    ///
413    /// # Returns
414    ///
415    /// Complexity metrics including property counts, nesting depth, and reference counts
416    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        // Calculate max nesting depth
425        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    /// Calculates the maximum nesting depth of entity references
439    fn calculate_max_nesting_depth(&self) -> usize {
440        let mut max_depth = 1; // Aspect itself is depth 1
441
442        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    /// Helper to calculate depth of a characteristic (handles nested collections)
454    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; // Avoid infinite recursion on circular refs
460        }
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    /// Finds properties by name pattern (case-insensitive)
492    ///
493    /// # Arguments
494    ///
495    /// * `pattern` - Pattern to match against property names
496    ///
497    /// # Returns
498    ///
499    /// Vector of properties with names containing the pattern
500    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    /// Groups properties by their characteristic type
510    ///
511    /// # Returns
512    ///
513    /// HashMap mapping characteristic type names to vectors of properties
514    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    /// Gets the aspect reference
547    pub fn aspect(&self) -> &Aspect {
548        self.aspect
549    }
550
551    /// Find properties by fuzzy name matching
552    ///
553    /// Uses Levenshtein distance to find properties whose names are similar to the query.
554    /// Useful for finding properties when you don't know the exact name.
555    ///
556    /// # Arguments
557    ///
558    /// * `query` - The property name to search for (partial or misspelled)
559    /// * `max_distance` - Maximum Levenshtein distance (lower = stricter matching)
560    ///
561    /// # Returns
562    ///
563    /// Vector of (property, distance) tuples, sorted by distance (best matches first)
564    ///
565    /// # Examples
566    ///
567    /// ```rust,ignore
568    /// use oxirs_samm::query::ModelQuery;
569    /// # use oxirs_samm::metamodel::{Aspect, Property};
570    /// # fn example(aspect: &Aspect) {
571    /// let query = ModelQuery::new(aspect);
572    ///
573    /// // Find properties with names similar to "temperture" (misspelled)
574    /// let results = query.fuzzy_find_properties("temperture", 3);
575    /// for (property, distance) in results {
576    ///     println!("Found: {} (distance: {})", property.name(), distance);
577    /// }
578    /// # }
579    /// ```
580    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        // Sort by distance (best matches first)
597        results.sort_by_key(|(_, distance)| *distance);
598        results
599    }
600
601    /// Find operations by fuzzy name matching
602    ///
603    /// Uses Levenshtein distance to find operations whose names are similar to the query.
604    ///
605    /// # Arguments
606    ///
607    /// * `query` - The operation name to search for
608    /// * `max_distance` - Maximum Levenshtein distance
609    ///
610    /// # Returns
611    ///
612    /// Vector of (operation, distance) tuples, sorted by distance
613    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    /// Find all model elements (properties, operations) by fuzzy search
634    ///
635    /// Searches across all element names in the aspect.
636    ///
637    /// # Arguments
638    ///
639    /// * `query` - The element name to search for
640    /// * `max_distance` - Maximum Levenshtein distance
641    ///
642    /// # Returns
643    ///
644    /// Vector of (element name, URN, distance) tuples, sorted by distance
645    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        // Search properties
653        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        // Search operations
661        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        // Sort by distance
669        results.sort_by_key(|(_, _, distance)| *distance);
670        results
671    }
672
673    /// Find properties with similar names (auto-suggest)
674    ///
675    /// Provides auto-complete style suggestions based on prefix matching
676    /// combined with fuzzy matching as fallback.
677    ///
678    /// # Arguments
679    ///
680    /// * `prefix` - The partial property name to match
681    /// * `limit` - Maximum number of suggestions to return
682    ///
683    /// # Returns
684    ///
685    /// Vector of suggested property names
686    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        // First, collect exact prefix matches
691        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        // Then, collect fuzzy matches (not already in prefix matches)
700        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        // Combine results
718        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/// Calculate Levenshtein distance between two strings
728///
729/// The Levenshtein distance is the minimum number of single-character edits
730/// (insertions, deletions, or substitutions) required to change one string into another.
731///
732/// # Arguments
733///
734/// * `a` - First string
735/// * `b` - Second string
736///
737/// # Returns
738///
739/// The Levenshtein distance as a usize
740#[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    // Initialize first row and column
755    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    // Compute distances
763    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, // deletion
772                matrix[i + 1][j] + 1, // insertion
773                matrix[i][j] + cost,  // substitution
774            ]
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    // Fuzzy Search Tests
989
990    #[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); // exact match
1012    }
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        // "temperture" is missing an 'a' - distance of 1
1022        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        // Should be sorted by distance
1039        assert!(!results.is_empty());
1040        assert_eq!(results[0].0.name(), "temp"); // exact match first
1041        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); // missing 'e'
1054
1055        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        // "temp" vs "temperature" has distance of 7 (need to add "erature")
1067        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        // "temper" doesn't match "humidity", but fuzzy match should find "temperature"
1109        let suggestions = query.suggest_properties("temper", 5);
1110
1111        assert!(!suggestions.is_empty());
1112        assert!(suggestions.contains(&"temperature".to_string()));
1113    }
1114}