1use crate::error::{BenchError, Result};
10use mockforge_core::openapi::spec::OpenApiSpec;
11use serde::{Deserialize, Serialize};
12use std::collections::{HashMap, HashSet};
13use std::path::{Path, PathBuf};
14
15#[derive(Debug, Clone, Serialize, Deserialize, Default)]
17pub struct SpecDependencyConfig {
18 #[serde(default)]
20 pub execution_order: Vec<SpecGroup>,
21 #[serde(default)]
23 pub disable_auto_detect: bool,
24}
25
26impl SpecDependencyConfig {
27 pub fn from_file(path: &Path) -> Result<Self> {
29 let content = std::fs::read_to_string(path)
30 .map_err(|e| BenchError::Other(format!("Failed to read dependency config: {}", e)))?;
31
32 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
33 match ext {
34 "yaml" | "yml" => serde_yaml::from_str(&content).map_err(|e| {
35 BenchError::Other(format!("Failed to parse YAML dependency config: {}", e))
36 }),
37 "json" => serde_json::from_str(&content).map_err(|e| {
38 BenchError::Other(format!("Failed to parse JSON dependency config: {}", e))
39 }),
40 _ => Err(BenchError::Other(format!(
41 "Unsupported dependency config format: {}. Use .yaml, .yml, or .json",
42 ext
43 ))),
44 }
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct SpecGroup {
51 pub name: String,
53 pub specs: Vec<PathBuf>,
55 #[serde(default)]
57 pub extract: HashMap<String, String>,
58 #[serde(default)]
60 pub inject: HashMap<String, String>,
61}
62
63#[derive(Debug, Clone)]
65pub struct SpecDependency {
66 pub dependent_spec: PathBuf,
68 pub dependency_spec: PathBuf,
70 pub field_name: String,
72 pub referenced_schema: String,
74 pub extraction_path: String,
76}
77
78pub struct DependencyDetector {
80 schema_registry: HashMap<PathBuf, HashSet<String>>,
82 dependencies: Vec<SpecDependency>,
84}
85
86impl DependencyDetector {
87 pub fn new() -> Self {
89 Self {
90 schema_registry: HashMap::new(),
91 dependencies: Vec::new(),
92 }
93 }
94
95 pub fn detect_dependencies(&mut self, specs: &[(PathBuf, OpenApiSpec)]) -> Vec<SpecDependency> {
97 for (path, spec) in specs {
99 let schemas = self.extract_schema_names(spec);
100 self.schema_registry.insert(path.clone(), schemas);
101 }
102
103 for (path, spec) in specs {
105 self.analyze_spec_references(path, spec, specs);
106 }
107
108 self.dependencies.clone()
109 }
110
111 fn extract_schema_names(&self, spec: &OpenApiSpec) -> HashSet<String> {
113 let mut schemas = HashSet::new();
114
115 if let Some(components) = &spec.spec.components {
116 for (name, _) in &components.schemas {
117 schemas.insert(name.clone());
118 schemas.insert(name.to_lowercase());
120 schemas.insert(to_snake_case(name));
121 }
122 }
123
124 schemas
125 }
126
127 fn analyze_spec_references(
129 &mut self,
130 current_path: &PathBuf,
131 spec: &OpenApiSpec,
132 all_specs: &[(PathBuf, OpenApiSpec)],
133 ) {
134 for (path, path_item) in &spec.spec.paths.paths {
136 if let openapiv3::ReferenceOr::Item(item) = path_item {
137 if let Some(op) = &item.post {
139 self.analyze_operation_refs(current_path, op, all_specs, path);
140 }
141 if let Some(op) = &item.put {
142 self.analyze_operation_refs(current_path, op, all_specs, path);
143 }
144 if let Some(op) = &item.patch {
145 self.analyze_operation_refs(current_path, op, all_specs, path);
146 }
147 }
148 }
149 }
150
151 fn analyze_operation_refs(
153 &mut self,
154 current_path: &PathBuf,
155 operation: &openapiv3::Operation,
156 all_specs: &[(PathBuf, OpenApiSpec)],
157 _api_path: &str,
158 ) {
159 if let Some(request_body) = &operation.request_body {
160 if let openapiv3::ReferenceOr::Item(body) = request_body {
161 if let Some(media_type) = body.content.get("application/json") {
163 if let Some(schema_ref) = &media_type.schema {
164 self.analyze_schema_for_refs(current_path, schema_ref, all_specs, "");
165 }
166 }
167 }
168 }
169 }
170
171 fn analyze_schema_for_refs(
173 &mut self,
174 current_path: &PathBuf,
175 schema_ref: &openapiv3::ReferenceOr<openapiv3::Schema>,
176 all_specs: &[(PathBuf, OpenApiSpec)],
177 field_prefix: &str,
178 ) {
179 match schema_ref {
180 openapiv3::ReferenceOr::Item(schema) => {
181 self.analyze_schema(current_path, schema, all_specs, field_prefix);
182 }
183 openapiv3::ReferenceOr::Reference { reference } => {
184 let _ = reference; }
187 }
188 }
189
190 fn analyze_schema(
192 &mut self,
193 current_path: &PathBuf,
194 schema: &openapiv3::Schema,
195 all_specs: &[(PathBuf, OpenApiSpec)],
196 field_prefix: &str,
197 ) {
198 match &schema.schema_kind {
199 openapiv3::SchemaKind::Type(openapiv3::Type::Object(obj)) => {
200 for (prop_name, prop_schema) in &obj.properties {
201 let full_path = if field_prefix.is_empty() {
202 prop_name.clone()
203 } else {
204 format!("{}.{}", field_prefix, prop_name)
205 };
206
207 if let Some(dep) = self.detect_ref_field(current_path, prop_name, all_specs) {
209 self.dependencies.push(SpecDependency {
210 dependent_spec: current_path.clone(),
211 dependency_spec: dep.0,
212 field_name: prop_name.clone(),
213 referenced_schema: dep.1,
214 extraction_path: format!("$.{}", full_path),
215 });
216 }
217
218 self.analyze_boxed_schema_ref(current_path, prop_schema, all_specs, &full_path);
220 }
221 }
222 openapiv3::SchemaKind::AllOf { all_of } => {
223 for sub_schema in all_of {
224 self.analyze_schema_for_refs(current_path, sub_schema, all_specs, field_prefix);
225 }
226 }
227 openapiv3::SchemaKind::OneOf { one_of } => {
228 for sub_schema in one_of {
229 self.analyze_schema_for_refs(current_path, sub_schema, all_specs, field_prefix);
230 }
231 }
232 openapiv3::SchemaKind::AnyOf { any_of } => {
233 for sub_schema in any_of {
234 self.analyze_schema_for_refs(current_path, sub_schema, all_specs, field_prefix);
235 }
236 }
237 _ => {}
238 }
239 }
240
241 fn analyze_boxed_schema_ref(
243 &mut self,
244 current_path: &PathBuf,
245 schema_ref: &openapiv3::ReferenceOr<Box<openapiv3::Schema>>,
246 all_specs: &[(PathBuf, OpenApiSpec)],
247 field_prefix: &str,
248 ) {
249 match schema_ref {
250 openapiv3::ReferenceOr::Item(boxed_schema) => {
251 self.analyze_schema(current_path, boxed_schema.as_ref(), all_specs, field_prefix);
252 }
253 openapiv3::ReferenceOr::Reference { reference } => {
254 let _ = reference; }
256 }
257 }
258
259 fn detect_ref_field(
261 &self,
262 current_path: &PathBuf,
263 field_name: &str,
264 all_specs: &[(PathBuf, OpenApiSpec)],
265 ) -> Option<(PathBuf, String)> {
266 let ref_patterns = [
268 ("_ref", ""), ("_id", ""), ("Id", ""), ("_uuid", ""), ("Uuid", ""), ("_reference", ""), ];
275
276 for (suffix, _) in ref_patterns.iter() {
277 if field_name.ends_with(suffix) {
278 let schema_base = field_name.trim_end_matches(suffix).trim_end_matches('_');
280
281 for (other_path, _) in all_specs {
283 if other_path == current_path {
284 continue;
285 }
286
287 if let Some(schemas) = self.schema_registry.get(other_path) {
288 let schema_pascal = to_pascal_case(schema_base);
290 let schema_lower = schema_base.to_lowercase();
291
292 for schema_name in schemas {
293 if schema_name == &schema_pascal
294 || schema_name == &schema_lower
295 || schema_name.to_lowercase() == schema_lower
296 {
297 return Some((other_path.clone(), schema_name.clone()));
298 }
299 }
300 }
301 }
302 }
303 }
304
305 None
306 }
307}
308
309impl Default for DependencyDetector {
310 fn default() -> Self {
311 Self::new()
312 }
313}
314
315pub fn topological_sort(
317 specs: &[(PathBuf, OpenApiSpec)],
318 dependencies: &[SpecDependency],
319) -> Result<Vec<PathBuf>> {
320 let spec_paths: Vec<PathBuf> = specs.iter().map(|(p, _)| p.clone()).collect();
321
322 let mut adj: HashMap<PathBuf, Vec<PathBuf>> = HashMap::new();
324 let mut in_degree: HashMap<PathBuf, usize> = HashMap::new();
325
326 for path in &spec_paths {
327 adj.insert(path.clone(), Vec::new());
328 in_degree.insert(path.clone(), 0);
329 }
330
331 for dep in dependencies {
332 adj.entry(dep.dependency_spec.clone())
333 .or_default()
334 .push(dep.dependent_spec.clone());
335 *in_degree.entry(dep.dependent_spec.clone()).or_insert(0) += 1;
336 }
337
338 let mut queue: Vec<PathBuf> = in_degree
340 .iter()
341 .filter(|(_, °)| deg == 0)
342 .map(|(path, _)| path.clone())
343 .collect();
344
345 let mut result = Vec::new();
346
347 while let Some(path) = queue.pop() {
348 result.push(path.clone());
349
350 if let Some(dependents) = adj.get(&path) {
351 for dependent in dependents {
352 if let Some(deg) = in_degree.get_mut(dependent) {
353 *deg -= 1;
354 if *deg == 0 {
355 queue.push(dependent.clone());
356 }
357 }
358 }
359 }
360 }
361
362 if result.len() != spec_paths.len() {
363 return Err(BenchError::Other("Circular dependency detected between specs".to_string()));
364 }
365
366 Ok(result)
367}
368
369fn to_snake_case(s: &str) -> String {
371 let mut result = String::new();
372 for (i, c) in s.chars().enumerate() {
373 if c.is_uppercase() {
374 if i > 0 {
375 result.push('_');
376 }
377 result.push(c.to_lowercase().next().unwrap());
378 } else {
379 result.push(c);
380 }
381 }
382 result
383}
384
385fn to_pascal_case(s: &str) -> String {
387 let mut result = String::new();
388 let mut capitalize_next = true;
389
390 for c in s.chars() {
391 if c == '_' || c == '-' {
392 capitalize_next = true;
393 } else if capitalize_next {
394 result.push(c.to_uppercase().next().unwrap());
395 capitalize_next = false;
396 } else {
397 result.push(c);
398 }
399 }
400
401 result
402}
403
404#[derive(Debug, Clone, Default)]
406pub struct ExtractedValues {
407 pub values: HashMap<String, serde_json::Value>,
409}
410
411impl ExtractedValues {
412 pub fn new() -> Self {
414 Self::default()
415 }
416
417 pub fn set(&mut self, key: String, value: serde_json::Value) {
419 self.values.insert(key, value);
420 }
421
422 pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
424 self.values.get(key)
425 }
426
427 pub fn merge(&mut self, other: &ExtractedValues) {
429 for (key, value) in &other.values {
430 self.values.insert(key.clone(), value.clone());
431 }
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
440 fn test_to_snake_case() {
441 assert_eq!(to_snake_case("PascalCase"), "pascal_case");
442 assert_eq!(to_snake_case("camelCase"), "camel_case");
443 assert_eq!(to_snake_case("Pool"), "pool");
444 assert_eq!(to_snake_case("VirtualService"), "virtual_service");
445 }
446
447 #[test]
448 fn test_to_pascal_case() {
449 assert_eq!(to_pascal_case("snake_case"), "SnakeCase");
450 assert_eq!(to_pascal_case("pool"), "Pool");
451 assert_eq!(to_pascal_case("virtual_service"), "VirtualService");
452 }
453
454 #[test]
455 fn test_extracted_values() {
456 let mut values = ExtractedValues::new();
457 values.set("pool_id".to_string(), serde_json::json!("abc123"));
458 values.set("name".to_string(), serde_json::json!("test-pool"));
459
460 assert_eq!(values.get("pool_id"), Some(&serde_json::json!("abc123")));
461 assert_eq!(values.get("missing"), None);
462 }
463
464 #[test]
465 fn test_spec_dependency_config_default() {
466 let config = SpecDependencyConfig::default();
467 assert!(config.execution_order.is_empty());
468 assert!(!config.disable_auto_detect);
469 }
470}