mockforge_intelligence/intelligent_behavior/
relationship_inference.rs1use mockforge_foundation::Result;
8use mockforge_openapi::OpenApiSpec;
9use openapiv3::{ReferenceOr, Schema};
10
11#[derive(Debug, Clone)]
13pub struct Relationship {
14 pub parent_entity: String,
16 pub child_entity: String,
18 pub count_field: Option<String>,
20 pub foreign_key_field: Option<String>,
22 pub relationship_path: Option<String>,
24 pub method: String,
26}
27
28impl Relationship {
29 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 pub fn with_count_field(mut self, field: String) -> Self {
43 self.count_field = Some(field);
44 self
45 }
46
47 pub fn with_foreign_key_field(mut self, field: String) -> Self {
49 self.foreign_key_field = Some(field);
50 self
51 }
52
53 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
61pub struct RelationshipInference {
63 relationships: Vec<Relationship>,
65}
66
67impl RelationshipInference {
68 pub fn new() -> Self {
70 Self {
71 relationships: Vec::new(),
72 }
73 }
74
75 pub fn infer_relationships(&mut self, spec: &OpenApiSpec) -> Result<Vec<Relationship>> {
77 self.relationships.clear();
78
79 self.infer_from_paths(spec)?;
82
83 self.infer_from_schemas(spec)?;
86
87 Ok(self.relationships.clone())
88 }
89
90 fn infer_from_paths(&mut self, spec: &OpenApiSpec) -> Result<()> {
92 let paths = &spec.spec.paths.paths;
97 for (path, path_item) in paths.iter() {
98 let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
101
102 if parts.len() >= 4 {
103 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 if id_param.starts_with('{') && id_param.ends_with('}') {
113 let parent_entity = parent.trim_end_matches('s'); let child_entity = child.trim_end_matches('s'); 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 fn infer_from_schemas(&mut self, spec: &OpenApiSpec) -> Result<()> {
150 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 fn analyze_schema_for_relationships(
165 &mut self,
166 _spec: &OpenApiSpec,
167 schema_name: &str,
168 schema: &Schema,
169 ) -> Result<()> {
170 let entity_name = schema_name.to_lowercase();
172
173 if let openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) = &schema.schema_kind {
175 for (prop_name, _prop_schema) in &obj.properties {
177 let prop_lower = prop_name.to_lowercase();
178
179 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 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 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 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 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 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}