Skip to main content

mockforge_intelligence/intelligent_behavior/
relationship_inference.rs

1//! Relationship Inference for Smart Personas
2//!
3//! This module provides functionality to automatically detect and infer
4//! relationships between entities in OpenAPI specifications, enabling
5//! automatic generation of related data.
6
7use mockforge_foundation::Result;
8use mockforge_openapi::OpenApiSpec;
9use openapiv3::{ReferenceOr, Schema};
10
11/// Represents a relationship between two entities
12#[derive(Debug, Clone)]
13pub struct Relationship {
14    /// Parent entity name (e.g., "apiary")
15    pub parent_entity: String,
16    /// Child entity name (e.g., "hive")
17    pub child_entity: String,
18    /// Field in parent that indicates count (e.g., "hive_count")
19    pub count_field: Option<String>,
20    /// Field in child that references parent (e.g., "apiary_id")
21    pub foreign_key_field: Option<String>,
22    /// API path for the relationship (e.g., "/api/apiaries/{id}/hives")
23    pub relationship_path: Option<String>,
24    /// HTTP method for the relationship endpoint
25    pub method: String,
26}
27
28impl Relationship {
29    /// Create a new relationship
30    pub fn new(parent_entity: String, child_entity: String) -> Self {
31        Self {
32            parent_entity,
33            child_entity,
34            count_field: None,
35            foreign_key_field: None,
36            relationship_path: None,
37            method: "GET".to_string(),
38        }
39    }
40
41    /// Set the count field
42    pub fn with_count_field(mut self, field: String) -> Self {
43        self.count_field = Some(field);
44        self
45    }
46
47    /// Set the foreign key field
48    pub fn with_foreign_key_field(mut self, field: String) -> Self {
49        self.foreign_key_field = Some(field);
50        self
51    }
52
53    /// Set the relationship path
54    pub fn with_path(mut self, path: String) -> Self {
55        self.relationship_path = Some(path);
56        self.method = "GET".to_string();
57        self
58    }
59}
60
61/// Relationship inference engine
62pub struct RelationshipInference {
63    /// Detected relationships
64    relationships: Vec<Relationship>,
65}
66
67impl RelationshipInference {
68    /// Create a new relationship inference engine
69    pub fn new() -> Self {
70        Self {
71            relationships: Vec::new(),
72        }
73    }
74
75    /// Infer relationships from an OpenAPI specification
76    pub fn infer_relationships(&mut self, spec: &OpenApiSpec) -> Result<Vec<Relationship>> {
77        self.relationships.clear();
78
79        // Strategy 1: Path-based inference
80        // Look for patterns like /api/{parent}/{id}/{child}
81        self.infer_from_paths(spec)?;
82
83        // Strategy 2: Schema-based inference
84        // Look for foreign key patterns and count fields in schemas
85        self.infer_from_schemas(spec)?;
86
87        Ok(self.relationships.clone())
88    }
89
90    /// Infer relationships from API paths
91    fn infer_from_paths(&mut self, spec: &OpenApiSpec) -> Result<()> {
92        // Extract entity names from paths
93        // Pattern: /api/{parent_entity}/{id}/{child_entity}
94        // Example: /api/apiaries/{apiaryId}/hives
95
96        let paths = &spec.spec.paths.paths;
97        for (path, path_item) in paths.iter() {
98            // Check if path matches nested resource pattern
99            // Pattern: /api/{parent}/{id}/{child}
100            let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
101
102            if parts.len() >= 4 {
103                // Check if we have a pattern like: /api/{parent}/{id}/{child}
104                let parent_part = parts.get(1);
105                let id_part = parts.get(2);
106                let child_part = parts.get(3);
107
108                if let (Some(parent), Some(id_param), Some(child)) =
109                    (parent_part, id_part, child_part)
110                {
111                    // Check if middle part is an ID parameter (starts with { and ends with })
112                    if id_param.starts_with('{') && id_param.ends_with('}') {
113                        // Extract entity names
114                        let parent_entity = parent.trim_end_matches('s'); // Remove plural
115                        let child_entity = child.trim_end_matches('s'); // Remove plural
116
117                        // Check if this path has a GET operation
118                        let has_get = match path_item {
119                            ReferenceOr::Item(item) => item.get.is_some() || item.post.is_some(),
120                            ReferenceOr::Reference { .. } => false,
121                        };
122
123                        if has_get {
124                            let relationship = Relationship::new(
125                                parent_entity.to_string(),
126                                child_entity.to_string(),
127                            )
128                            .with_path(path.clone())
129                            .with_foreign_key_field(format!("{}_id", parent_entity));
130
131                            tracing::debug!(
132                                "Inferred relationship from path: {} -> {} (path: {})",
133                                parent_entity,
134                                child_entity,
135                                path
136                            );
137
138                            self.relationships.push(relationship);
139                        }
140                    }
141                }
142            }
143        }
144
145        Ok(())
146    }
147
148    /// Infer relationships from schemas
149    fn infer_from_schemas(&mut self, spec: &OpenApiSpec) -> Result<()> {
150        // Look for count fields and foreign key patterns in schemas
151        if let Some(components) = &spec.spec.components {
152            let schemas = &components.schemas;
153            for (schema_name, schema_ref) in schemas {
154                if let ReferenceOr::Item(schema) = schema_ref {
155                    self.analyze_schema_for_relationships(spec, schema_name, schema)?;
156                }
157            }
158        }
159
160        Ok(())
161    }
162
163    /// Analyze a schema for relationship indicators
164    fn analyze_schema_for_relationships(
165        &mut self,
166        _spec: &OpenApiSpec,
167        schema_name: &str,
168        schema: &Schema,
169    ) -> Result<()> {
170        // Extract entity name from schema name (e.g., "Apiary" -> "apiary")
171        let entity_name = schema_name.to_lowercase();
172
173        // Check if this schema has properties
174        if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) = &schema.schema_kind {
175            // Look for count fields (e.g., "hive_count", "apiary_count")
176            for (prop_name, _prop_schema) in &obj.properties {
177                let prop_lower = prop_name.to_lowercase();
178
179                // Pattern: {entity}_count indicates relationship to {entity}
180                if prop_lower.ends_with("_count") {
181                    let related_entity =
182                        prop_lower.strip_suffix("_count").unwrap_or("").to_string();
183
184                    if !related_entity.is_empty() && related_entity != entity_name {
185                        // Check if we already have this relationship
186                        let exists = self.relationships.iter().any(|r| {
187                            r.parent_entity == entity_name && r.child_entity == related_entity
188                        });
189
190                        if !exists {
191                            let relationship =
192                                Relationship::new(entity_name.clone(), related_entity.clone())
193                                    .with_count_field(prop_name.clone())
194                                    .with_foreign_key_field(format!("{}_id", entity_name));
195
196                            tracing::debug!(
197                                "Inferred relationship from count field: {} -> {} (count_field: {})",
198                                entity_name,
199                                related_entity,
200                                prop_name
201                            );
202
203                            self.relationships.push(relationship);
204                        }
205                    }
206                }
207
208                // Pattern: {entity}_id indicates foreign key to {entity}
209                if prop_lower.ends_with("_id") && prop_lower != "id" {
210                    let parent_entity = prop_lower.strip_suffix("_id").unwrap_or("").to_string();
211
212                    if !parent_entity.is_empty() && parent_entity != entity_name {
213                        // This entity has a foreign key to parent_entity
214                        // Check if we already have this relationship
215                        let exists = self.relationships.iter().any(|r| {
216                            r.parent_entity == parent_entity && r.child_entity == entity_name
217                        });
218
219                        if !exists {
220                            let relationship =
221                                Relationship::new(parent_entity.clone(), entity_name.clone())
222                                    .with_foreign_key_field(prop_name.clone());
223
224                            tracing::debug!(
225                                "Inferred relationship from foreign key: {} -> {} (fk_field: {})",
226                                parent_entity,
227                                entity_name,
228                                prop_name
229                            );
230
231                            self.relationships.push(relationship);
232                        }
233                    }
234                }
235            }
236        }
237
238        Ok(())
239    }
240
241    /// Get relationships for a specific parent entity
242    pub fn get_relationships_for_parent(&self, parent_entity: &str) -> Vec<&Relationship> {
243        self.relationships.iter().filter(|r| r.parent_entity == parent_entity).collect()
244    }
245
246    /// Get all relationships
247    pub fn get_all_relationships(&self) -> &[Relationship] {
248        &self.relationships
249    }
250}
251
252impl Default for RelationshipInference {
253    fn default() -> Self {
254        Self::new()
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_relationship_creation() {
264        let rel = Relationship::new("apiary".to_string(), "hive".to_string())
265            .with_count_field("hive_count".to_string())
266            .with_foreign_key_field("apiary_id".to_string())
267            .with_path("/api/apiaries/{id}/hives".to_string());
268
269        assert_eq!(rel.parent_entity, "apiary");
270        assert_eq!(rel.child_entity, "hive");
271        assert_eq!(rel.count_field, Some("hive_count".to_string()));
272        assert_eq!(rel.foreign_key_field, Some("apiary_id".to_string()));
273        assert_eq!(rel.relationship_path, Some("/api/apiaries/{id}/hives".to_string()));
274    }
275
276    #[test]
277    fn test_relationship_inference_new() {
278        let inference = RelationshipInference::new();
279        assert_eq!(inference.relationships.len(), 0);
280    }
281}