aperture_cli/spec/
transformer.rs

1use crate::cache::models::{
2    CachedApertureSecret, CachedCommand, CachedParameter, CachedRequestBody, CachedResponse,
3    CachedSecurityScheme, CachedSpec, SkippedEndpoint, CACHE_FORMAT_VERSION,
4};
5use crate::error::Error;
6use openapiv3::{OpenAPI, Operation, Parameter, ReferenceOr, RequestBody, SecurityScheme};
7use serde_json;
8use std::collections::HashMap;
9
10/// Transforms `OpenAPI` specifications into Aperture's cached format
11pub struct SpecTransformer;
12
13impl SpecTransformer {
14    /// Creates a new `SpecTransformer` instance
15    #[must_use]
16    pub const fn new() -> Self {
17        Self
18    }
19
20    /// Transforms an `OpenAPI` specification into a cached representation
21    ///
22    /// This method converts the full `OpenAPI` spec into an optimized format
23    /// that can be quickly loaded and used for CLI generation.
24    ///
25    /// # Errors
26    ///
27    /// Returns an error if parameter reference resolution fails
28    pub fn transform(&self, name: &str, spec: &OpenAPI) -> Result<CachedSpec, Error> {
29        self.transform_with_filter(name, spec, &[])
30    }
31
32    /// Transforms an `OpenAPI` specification into a cached representation with endpoint filtering
33    ///
34    /// This method converts the full `OpenAPI` spec into an optimized format
35    /// that can be quickly loaded and used for CLI generation, filtering out specified endpoints.
36    ///
37    /// # Arguments
38    ///
39    /// * `name` - The name for the cached spec
40    /// * `spec` - The `OpenAPI` specification to transform
41    /// * `skip_endpoints` - List of endpoints to skip (path, method pairs)
42    ///
43    /// # Errors
44    ///
45    /// Returns an error if parameter reference resolution fails
46    pub fn transform_with_filter(
47        &self,
48        name: &str,
49        spec: &OpenAPI,
50        skip_endpoints: &[(String, String)],
51    ) -> Result<CachedSpec, Error> {
52        self.transform_with_warnings(name, spec, skip_endpoints, &[])
53    }
54
55    /// Transforms an `OpenAPI` specification with full warning information
56    ///
57    /// # Arguments
58    ///
59    /// * `name` - The name for the cached spec
60    /// * `spec` - The `OpenAPI` specification to transform
61    /// * `skip_endpoints` - List of endpoints to skip (path, method pairs)
62    /// * `warnings` - Validation warnings to store in the cached spec
63    ///
64    /// # Errors
65    ///
66    /// Returns an error if parameter reference resolution fails
67    pub fn transform_with_warnings(
68        &self,
69        name: &str,
70        spec: &OpenAPI,
71        skip_endpoints: &[(String, String)],
72        warnings: &[crate::spec::validator::ValidationWarning],
73    ) -> Result<CachedSpec, Error> {
74        let mut commands = Vec::new();
75
76        // Extract version from info
77        let version = spec.info.version.clone();
78
79        // Extract server URLs
80        let servers: Vec<String> = spec.servers.iter().map(|s| s.url.clone()).collect();
81        let base_url = servers.first().cloned();
82
83        // Extract global security requirements
84        let global_security_requirements: Vec<String> = spec
85            .security
86            .iter()
87            .flat_map(|security_vec| {
88                security_vec
89                    .iter()
90                    .flat_map(|security_req| security_req.keys().cloned())
91            })
92            .collect();
93
94        // Process all paths and operations
95        for (path, path_item) in spec.paths.iter() {
96            Self::process_path_item(
97                spec,
98                path,
99                path_item,
100                skip_endpoints,
101                &global_security_requirements,
102                &mut commands,
103            )?;
104        }
105
106        // Extract security schemes
107        let security_schemes = Self::extract_security_schemes(spec);
108
109        // Convert warnings to skipped endpoints
110        let skipped_endpoints: Vec<SkippedEndpoint> = warnings
111            .iter()
112            .map(|w| SkippedEndpoint {
113                path: w.endpoint.path.clone(),
114                method: w.endpoint.method.clone(),
115                content_type: w.endpoint.content_type.clone(),
116                reason: w.reason.clone(),
117            })
118            .collect();
119
120        Ok(CachedSpec {
121            cache_format_version: CACHE_FORMAT_VERSION,
122            name: name.to_string(),
123            version,
124            commands,
125            base_url,
126            servers,
127            security_schemes,
128            skipped_endpoints,
129        })
130    }
131
132    /// Process a single path item and its operations
133    fn process_path_item(
134        spec: &OpenAPI,
135        path: &str,
136        path_item: &ReferenceOr<openapiv3::PathItem>,
137        skip_endpoints: &[(String, String)],
138        global_security_requirements: &[String],
139        commands: &mut Vec<CachedCommand>,
140    ) -> Result<(), Error> {
141        let ReferenceOr::Item(item) = path_item else {
142            return Ok(());
143        };
144
145        // Process each HTTP method
146        for (method, operation) in crate::spec::http_methods_iter(item) {
147            let Some(op) = operation else {
148                continue;
149            };
150
151            if Self::should_skip_endpoint(path, method, skip_endpoints) {
152                continue;
153            }
154
155            let command =
156                Self::transform_operation(spec, method, path, op, global_security_requirements)?;
157            commands.push(command);
158        }
159
160        Ok(())
161    }
162
163    /// Check if an endpoint should be skipped
164    fn should_skip_endpoint(path: &str, method: &str, skip_endpoints: &[(String, String)]) -> bool {
165        skip_endpoints.iter().any(|(skip_path, skip_method)| {
166            skip_path == path && skip_method.eq_ignore_ascii_case(method)
167        })
168    }
169
170    /// Transforms a single operation into a cached command
171    fn transform_operation(
172        spec: &OpenAPI,
173        method: &str,
174        path: &str,
175        operation: &Operation,
176        global_security_requirements: &[String],
177    ) -> Result<CachedCommand, Error> {
178        // Extract operation metadata
179        let operation_id = operation
180            .operation_id
181            .clone()
182            .unwrap_or_else(|| format!("{method}_{path}"));
183
184        // Use first tag as command namespace, or "default" if no tags
185        let name = operation
186            .tags
187            .first()
188            .cloned()
189            .unwrap_or_else(|| "default".to_string());
190
191        // Transform parameters
192        let mut parameters = Vec::new();
193        for param_ref in &operation.parameters {
194            match param_ref {
195                ReferenceOr::Item(param) => {
196                    parameters.push(Self::transform_parameter(param));
197                }
198                ReferenceOr::Reference { reference } => {
199                    let param = Self::resolve_parameter_reference(spec, reference)?;
200                    parameters.push(Self::transform_parameter(&param));
201                }
202            }
203        }
204
205        // Transform request body
206        let request_body = operation
207            .request_body
208            .as_ref()
209            .and_then(Self::transform_request_body);
210
211        // Transform responses
212        let responses = operation
213            .responses
214            .responses
215            .iter()
216            .map(|(code, response_ref)| {
217                match response_ref {
218                    ReferenceOr::Item(response) => {
219                        // Get description
220                        let description = if response.description.is_empty() {
221                            None
222                        } else {
223                            Some(response.description.clone())
224                        };
225
226                        // Get first content type and schema if available
227                        let (content_type, schema) =
228                            if let Some((ct, media_type)) = response.content.iter().next() {
229                                let schema = media_type.schema.as_ref().and_then(|schema_ref| {
230                                    match schema_ref {
231                                        ReferenceOr::Item(schema) => {
232                                            serde_json::to_string(schema).ok()
233                                        }
234                                        ReferenceOr::Reference { .. } => None,
235                                    }
236                                });
237                                (Some(ct.clone()), schema)
238                            } else {
239                                (None, None)
240                            };
241
242                        CachedResponse {
243                            status_code: code.to_string(),
244                            description,
245                            content_type,
246                            schema,
247                        }
248                    }
249                    ReferenceOr::Reference { .. } => CachedResponse {
250                        status_code: code.to_string(),
251                        description: None,
252                        content_type: None,
253                        schema: None,
254                    },
255                }
256            })
257            .collect();
258
259        // Extract security requirements - use operation-level if defined, else global
260        let security_requirements = operation.security.as_ref().map_or_else(
261            || global_security_requirements.to_vec(),
262            |security_reqs| {
263                security_reqs
264                    .iter()
265                    .flat_map(|security_req| security_req.keys().cloned())
266                    .collect()
267            },
268        );
269
270        Ok(CachedCommand {
271            name,
272            description: operation.description.clone(),
273            summary: operation.summary.clone(),
274            operation_id,
275            method: method.to_uppercase(),
276            path: path.to_string(),
277            parameters,
278            request_body,
279            responses,
280            security_requirements,
281            tags: operation.tags.clone(),
282            deprecated: operation.deprecated,
283            external_docs_url: operation
284                .external_docs
285                .as_ref()
286                .map(|docs| docs.url.clone()),
287        })
288    }
289
290    /// Transforms a parameter into cached format
291    #[allow(clippy::too_many_lines)]
292    fn transform_parameter(param: &Parameter) -> CachedParameter {
293        let (param_data, location_str) = match param {
294            Parameter::Query { parameter_data, .. } => (parameter_data, "query"),
295            Parameter::Header { parameter_data, .. } => (parameter_data, "header"),
296            Parameter::Path { parameter_data, .. } => (parameter_data, "path"),
297            Parameter::Cookie { parameter_data, .. } => (parameter_data, "cookie"),
298        };
299
300        // Extract schema information from parameter
301        let (schema_json, schema_type, format, default_value, enum_values) =
302            if let openapiv3::ParameterSchemaOrContent::Schema(schema_ref) = &param_data.format {
303                match schema_ref {
304                    ReferenceOr::Item(schema) => {
305                        let schema_json = serde_json::to_string(schema).ok();
306
307                        // Extract type information
308                        let (schema_type, format, default, enums) = match &schema.schema_kind {
309                            openapiv3::SchemaKind::Type(type_val) => match type_val {
310                                openapiv3::Type::String(string_type) => {
311                                    let enum_values: Vec<String> = string_type
312                                        .enumeration
313                                        .iter()
314                                        .filter_map(|v| v.as_ref())
315                                        .map(|v| {
316                                            serde_json::to_string(v)
317                                                .unwrap_or_else(|_| v.to_string())
318                                        })
319                                        .collect();
320                                    let format = match &string_type.format {
321                                        openapiv3::VariantOrUnknownOrEmpty::Item(fmt) => {
322                                            Some(format!("{fmt:?}"))
323                                        }
324                                        _ => None,
325                                    };
326                                    ("string".to_string(), format, None, enum_values)
327                                }
328                                openapiv3::Type::Number(number_type) => {
329                                    let format = match &number_type.format {
330                                        openapiv3::VariantOrUnknownOrEmpty::Item(fmt) => {
331                                            Some(format!("{fmt:?}"))
332                                        }
333                                        _ => None,
334                                    };
335                                    ("number".to_string(), format, None, vec![])
336                                }
337                                openapiv3::Type::Integer(integer_type) => {
338                                    let format = match &integer_type.format {
339                                        openapiv3::VariantOrUnknownOrEmpty::Item(fmt) => {
340                                            Some(format!("{fmt:?}"))
341                                        }
342                                        _ => None,
343                                    };
344                                    ("integer".to_string(), format, None, vec![])
345                                }
346                                openapiv3::Type::Boolean(_) => {
347                                    ("boolean".to_string(), None, None, vec![])
348                                }
349                                openapiv3::Type::Array(_) => {
350                                    ("array".to_string(), None, None, vec![])
351                                }
352                                openapiv3::Type::Object(_) => {
353                                    ("object".to_string(), None, None, vec![])
354                                }
355                            },
356                            _ => ("string".to_string(), None, None, vec![]),
357                        };
358
359                        // Extract default value if present
360                        let default_value =
361                            schema.schema_data.default.as_ref().map(|v| {
362                                serde_json::to_string(v).unwrap_or_else(|_| v.to_string())
363                            });
364
365                        (
366                            schema_json,
367                            Some(schema_type),
368                            format,
369                            default_value.or(default),
370                            enums,
371                        )
372                    }
373                    ReferenceOr::Reference { .. } => {
374                        // For references, use basic defaults
375                        (
376                            Some(r#"{"type": "string"}"#.to_string()),
377                            Some("string".to_string()),
378                            None,
379                            None,
380                            vec![],
381                        )
382                    }
383                }
384            } else {
385                // No schema provided, use defaults
386                (
387                    Some(r#"{"type": "string"}"#.to_string()),
388                    Some("string".to_string()),
389                    None,
390                    None,
391                    vec![],
392                )
393            };
394
395        // Extract example value
396        let example = param_data
397            .example
398            .as_ref()
399            .map(|ex| serde_json::to_string(ex).unwrap_or_else(|_| ex.to_string()));
400
401        CachedParameter {
402            name: param_data.name.clone(),
403            location: location_str.to_string(),
404            required: param_data.required,
405            description: param_data.description.clone(),
406            schema: schema_json,
407            schema_type,
408            format,
409            default_value,
410            enum_values,
411            example,
412        }
413    }
414
415    /// Transforms a request body into cached format
416    fn transform_request_body(
417        request_body: &ReferenceOr<RequestBody>,
418    ) -> Option<CachedRequestBody> {
419        match request_body {
420            ReferenceOr::Item(body) => {
421                // Prefer JSON content if available
422                let content_type = if body.content.contains_key("application/json") {
423                    "application/json"
424                } else {
425                    body.content.keys().next()?
426                };
427
428                // Extract schema and example from the content
429                let media_type = body.content.get(content_type)?;
430                let schema = media_type
431                    .schema
432                    .as_ref()
433                    .and_then(|schema_ref| match schema_ref {
434                        ReferenceOr::Item(schema) => serde_json::to_string(schema).ok(),
435                        ReferenceOr::Reference { .. } => None,
436                    })
437                    .unwrap_or_else(|| "{}".to_string());
438
439                let example = media_type
440                    .example
441                    .as_ref()
442                    .map(|ex| serde_json::to_string(ex).unwrap_or_else(|_| ex.to_string()));
443
444                Some(CachedRequestBody {
445                    content_type: content_type.to_string(),
446                    schema,
447                    required: body.required,
448                    description: body.description.clone(),
449                    example,
450                })
451            }
452            ReferenceOr::Reference { .. } => None, // Skip references for now
453        }
454    }
455
456    /// Extracts and transforms security schemes from the `OpenAPI` spec
457    fn extract_security_schemes(spec: &OpenAPI) -> HashMap<String, CachedSecurityScheme> {
458        let mut security_schemes = HashMap::new();
459
460        if let Some(components) = &spec.components {
461            for (name, scheme_ref) in &components.security_schemes {
462                if let ReferenceOr::Item(scheme) = scheme_ref {
463                    if let Some(cached_scheme) = Self::transform_security_scheme(name, scheme) {
464                        security_schemes.insert(name.clone(), cached_scheme);
465                    }
466                }
467            }
468        }
469
470        security_schemes
471    }
472
473    /// Transforms a single security scheme into cached format
474    fn transform_security_scheme(
475        name: &str,
476        scheme: &SecurityScheme,
477    ) -> Option<CachedSecurityScheme> {
478        match scheme {
479            SecurityScheme::APIKey {
480                location,
481                name: param_name,
482                description,
483                ..
484            } => {
485                let aperture_secret = Self::extract_aperture_secret(scheme);
486                let location_str = match location {
487                    openapiv3::APIKeyLocation::Query => "query",
488                    openapiv3::APIKeyLocation::Header => "header",
489                    openapiv3::APIKeyLocation::Cookie => "cookie",
490                };
491
492                Some(CachedSecurityScheme {
493                    name: name.to_string(),
494                    scheme_type: "apiKey".to_string(),
495                    scheme: None,
496                    location: Some(location_str.to_string()),
497                    parameter_name: Some(param_name.clone()),
498                    description: description.clone(),
499                    bearer_format: None,
500                    aperture_secret,
501                })
502            }
503            SecurityScheme::HTTP {
504                scheme: http_scheme,
505                bearer_format,
506                description,
507                ..
508            } => {
509                let aperture_secret = Self::extract_aperture_secret(scheme);
510                Some(CachedSecurityScheme {
511                    name: name.to_string(),
512                    scheme_type: "http".to_string(),
513                    scheme: Some(http_scheme.clone()),
514                    location: Some("header".to_string()),
515                    parameter_name: Some("Authorization".to_string()),
516                    description: description.clone(),
517                    bearer_format: bearer_format.clone(),
518                    aperture_secret,
519                })
520            }
521            // OAuth2 and OpenID Connect should be rejected in validation
522            SecurityScheme::OAuth2 { .. } | SecurityScheme::OpenIDConnect { .. } => None,
523        }
524    }
525
526    /// Extracts x-aperture-secret extension from a security scheme
527    fn extract_aperture_secret(scheme: &SecurityScheme) -> Option<CachedApertureSecret> {
528        // Get extensions from the security scheme
529        let extensions = match scheme {
530            SecurityScheme::APIKey { extensions, .. } | SecurityScheme::HTTP { extensions, .. } => {
531                extensions
532            }
533            SecurityScheme::OAuth2 { .. } | SecurityScheme::OpenIDConnect { .. } => return None,
534        };
535
536        // Parse the x-aperture-secret extension
537        extensions.get("x-aperture-secret").and_then(|value| {
538            // The extension should be an object with "source" and "name" fields
539            if let Some(obj) = value.as_object() {
540                let source = obj.get("source")?.as_str()?;
541                let name = obj.get("name")?.as_str()?;
542
543                // Currently only "env" source is supported
544                if source == "env" {
545                    return Some(CachedApertureSecret {
546                        source: source.to_string(),
547                        name: name.to_string(),
548                    });
549                }
550            }
551            None
552        })
553    }
554
555    /// Resolves a parameter reference to its actual parameter definition
556    fn resolve_parameter_reference(spec: &OpenAPI, reference: &str) -> Result<Parameter, Error> {
557        crate::spec::resolve_parameter_reference(spec, reference)
558    }
559}
560
561impl Default for SpecTransformer {
562    fn default() -> Self {
563        Self::new()
564    }
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570    use openapiv3::{Info, OpenAPI};
571
572    fn create_test_spec() -> OpenAPI {
573        OpenAPI {
574            openapi: "3.0.0".to_string(),
575            info: Info {
576                title: "Test API".to_string(),
577                version: "1.0.0".to_string(),
578                ..Default::default()
579            },
580            servers: vec![openapiv3::Server {
581                url: "https://api.example.com".to_string(),
582                ..Default::default()
583            }],
584            paths: Default::default(),
585            ..Default::default()
586        }
587    }
588
589    #[test]
590    fn test_transform_basic_spec() {
591        let transformer = SpecTransformer::new();
592        let spec = create_test_spec();
593        let cached = transformer
594            .transform("test", &spec)
595            .expect("Transform should succeed");
596
597        assert_eq!(cached.name, "test");
598        assert_eq!(cached.version, "1.0.0");
599        assert_eq!(cached.base_url, Some("https://api.example.com".to_string()));
600        assert_eq!(cached.servers.len(), 1);
601        assert!(cached.commands.is_empty());
602    }
603
604    #[test]
605    fn test_transform_with_operations() {
606        use openapiv3::{Operation, PathItem, ReferenceOr, Responses};
607
608        let transformer = SpecTransformer::new();
609        let mut spec = create_test_spec();
610
611        let mut path_item = PathItem::default();
612        path_item.get = Some(Operation {
613            operation_id: Some("getUsers".to_string()),
614            tags: vec!["users".to_string()],
615            description: Some("Get all users".to_string()),
616            responses: Responses::default(),
617            ..Default::default()
618        });
619
620        spec.paths
621            .paths
622            .insert("/users".to_string(), ReferenceOr::Item(path_item));
623
624        let cached = transformer
625            .transform("test", &spec)
626            .expect("Transform should succeed");
627
628        assert_eq!(cached.commands.len(), 1);
629        let command = &cached.commands[0];
630        assert_eq!(command.name, "users");
631        assert_eq!(command.operation_id, "getUsers");
632        assert_eq!(command.method, "GET");
633        assert_eq!(command.path, "/users");
634        assert_eq!(command.description, Some("Get all users".to_string()));
635    }
636
637    #[test]
638    fn test_transform_with_parameter_reference() {
639        use openapiv3::{
640            Components, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem,
641            ReferenceOr, Responses, Schema, SchemaData, SchemaKind, Type,
642        };
643
644        let transformer = SpecTransformer::new();
645        let mut spec = create_test_spec();
646
647        // Add a parameter to components
648        let mut components = Components::default();
649        let user_id_param = Parameter::Path {
650            parameter_data: ParameterData {
651                name: "userId".to_string(),
652                description: Some("Unique identifier of the user".to_string()),
653                required: true,
654                deprecated: Some(false),
655                format: ParameterSchemaOrContent::Schema(ReferenceOr::Item(Schema {
656                    schema_data: SchemaData::default(),
657                    schema_kind: SchemaKind::Type(Type::String(Default::default())),
658                })),
659                example: None,
660                examples: Default::default(),
661                explode: None,
662                extensions: Default::default(),
663            },
664            style: Default::default(),
665        };
666        components
667            .parameters
668            .insert("userId".to_string(), ReferenceOr::Item(user_id_param));
669        spec.components = Some(components);
670
671        // Create operation with parameter reference
672        let mut path_item = PathItem::default();
673        path_item.get = Some(Operation {
674            operation_id: Some("getUserById".to_string()),
675            tags: vec!["users".to_string()],
676            parameters: vec![ReferenceOr::Reference {
677                reference: "#/components/parameters/userId".to_string(),
678            }],
679            responses: Responses::default(),
680            ..Default::default()
681        });
682
683        spec.paths
684            .paths
685            .insert("/users/{userId}".to_string(), ReferenceOr::Item(path_item));
686
687        let cached = transformer
688            .transform("test", &spec)
689            .expect("Transform should succeed with parameter reference");
690
691        // Verify the parameter was resolved
692        assert_eq!(cached.commands.len(), 1);
693        let command = &cached.commands[0];
694        assert_eq!(command.parameters.len(), 1);
695        let param = &command.parameters[0];
696        assert_eq!(param.name, "userId");
697        assert_eq!(param.location, "path");
698        assert!(param.required);
699        assert_eq!(
700            param.description,
701            Some("Unique identifier of the user".to_string())
702        );
703    }
704
705    #[test]
706    fn test_transform_with_invalid_parameter_reference() {
707        use openapiv3::{Operation, PathItem, ReferenceOr, Responses};
708
709        let transformer = SpecTransformer::new();
710        let mut spec = create_test_spec();
711
712        // Create operation with invalid parameter reference
713        let mut path_item = PathItem::default();
714        path_item.get = Some(Operation {
715            parameters: vec![ReferenceOr::Reference {
716                reference: "#/invalid/reference/format".to_string(),
717            }],
718            responses: Responses::default(),
719            ..Default::default()
720        });
721
722        spec.paths
723            .paths
724            .insert("/users".to_string(), ReferenceOr::Item(path_item));
725
726        let result = transformer.transform("test", &spec);
727        assert!(result.is_err());
728        match result.unwrap_err() {
729            crate::error::Error::Validation(msg) => {
730                assert!(msg.contains("Invalid parameter reference format"));
731            }
732            _ => panic!("Expected Validation error"),
733        }
734    }
735
736    #[test]
737    fn test_transform_with_missing_parameter_reference() {
738        use openapiv3::{Components, Operation, PathItem, ReferenceOr, Responses};
739
740        let transformer = SpecTransformer::new();
741        let mut spec = create_test_spec();
742
743        // Add empty components
744        spec.components = Some(Components::default());
745
746        // Create operation with reference to non-existent parameter
747        let mut path_item = PathItem::default();
748        path_item.get = Some(Operation {
749            parameters: vec![ReferenceOr::Reference {
750                reference: "#/components/parameters/nonExistent".to_string(),
751            }],
752            responses: Responses::default(),
753            ..Default::default()
754        });
755
756        spec.paths
757            .paths
758            .insert("/users".to_string(), ReferenceOr::Item(path_item));
759
760        let result = transformer.transform("test", &spec);
761        assert!(result.is_err());
762        match result.unwrap_err() {
763            crate::error::Error::Validation(msg) => {
764                assert!(msg.contains("Parameter 'nonExistent' not found in components"));
765            }
766            _ => panic!("Expected Validation error"),
767        }
768    }
769
770    #[test]
771    fn test_transform_with_nested_parameter_reference() {
772        use openapiv3::{
773            Components, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem,
774            ReferenceOr, Responses, Schema, SchemaData, SchemaKind, Type,
775        };
776
777        let transformer = SpecTransformer::new();
778        let mut spec = create_test_spec();
779
780        let mut components = Components::default();
781
782        // Add a parameter that references another parameter
783        components.parameters.insert(
784            "userIdRef".to_string(),
785            ReferenceOr::Reference {
786                reference: "#/components/parameters/userId".to_string(),
787            },
788        );
789
790        // Add the actual parameter
791        let user_id_param = Parameter::Path {
792            parameter_data: ParameterData {
793                name: "userId".to_string(),
794                description: Some("User ID parameter".to_string()),
795                required: true,
796                deprecated: Some(false),
797                format: ParameterSchemaOrContent::Schema(ReferenceOr::Item(Schema {
798                    schema_data: SchemaData::default(),
799                    schema_kind: SchemaKind::Type(Type::String(Default::default())),
800                })),
801                example: None,
802                examples: Default::default(),
803                explode: None,
804                extensions: Default::default(),
805            },
806            style: Default::default(),
807        };
808        components
809            .parameters
810            .insert("userId".to_string(), ReferenceOr::Item(user_id_param));
811        spec.components = Some(components);
812
813        // Create operation with nested parameter reference
814        let mut path_item = PathItem::default();
815        path_item.get = Some(Operation {
816            parameters: vec![ReferenceOr::Reference {
817                reference: "#/components/parameters/userIdRef".to_string(),
818            }],
819            responses: Responses::default(),
820            ..Default::default()
821        });
822
823        spec.paths
824            .paths
825            .insert("/users/{userId}".to_string(), ReferenceOr::Item(path_item));
826
827        let cached = transformer
828            .transform("test", &spec)
829            .expect("Transform should succeed with nested parameter reference");
830
831        // Verify the nested reference was resolved
832        assert_eq!(cached.commands.len(), 1);
833        let command = &cached.commands[0];
834        assert_eq!(command.parameters.len(), 1);
835        let param = &command.parameters[0];
836        assert_eq!(param.name, "userId");
837        assert_eq!(param.description, Some("User ID parameter".to_string()));
838    }
839
840    #[test]
841    fn test_transform_with_circular_parameter_reference() {
842        use openapiv3::{Components, Operation, PathItem, ReferenceOr, Responses};
843
844        let transformer = SpecTransformer::new();
845        let mut spec = create_test_spec();
846
847        let mut components = Components::default();
848
849        // Create direct circular reference: paramA -> paramA
850        components.parameters.insert(
851            "paramA".to_string(),
852            ReferenceOr::Reference {
853                reference: "#/components/parameters/paramA".to_string(),
854            },
855        );
856
857        spec.components = Some(components);
858
859        // Create operation with circular parameter reference
860        let mut path_item = PathItem::default();
861        path_item.get = Some(Operation {
862            parameters: vec![ReferenceOr::Reference {
863                reference: "#/components/parameters/paramA".to_string(),
864            }],
865            responses: Responses::default(),
866            ..Default::default()
867        });
868
869        spec.paths
870            .paths
871            .insert("/test".to_string(), ReferenceOr::Item(path_item));
872
873        let result = transformer.transform("test", &spec);
874        assert!(result.is_err());
875        match result.unwrap_err() {
876            crate::error::Error::Validation(msg) => {
877                assert!(
878                    msg.contains("Circular reference detected"),
879                    "Error message should mention circular reference: {}",
880                    msg
881                );
882            }
883            _ => panic!("Expected Validation error for circular reference"),
884        }
885    }
886
887    #[test]
888    fn test_transform_with_indirect_circular_reference() {
889        use openapiv3::{Components, Operation, PathItem, ReferenceOr, Responses};
890
891        let transformer = SpecTransformer::new();
892        let mut spec = create_test_spec();
893
894        let mut components = Components::default();
895
896        // Create indirect circular reference: paramA -> paramB -> paramA
897        components.parameters.insert(
898            "paramA".to_string(),
899            ReferenceOr::Reference {
900                reference: "#/components/parameters/paramB".to_string(),
901            },
902        );
903
904        components.parameters.insert(
905            "paramB".to_string(),
906            ReferenceOr::Reference {
907                reference: "#/components/parameters/paramA".to_string(),
908            },
909        );
910
911        spec.components = Some(components);
912
913        // Create operation with circular parameter reference
914        let mut path_item = PathItem::default();
915        path_item.get = Some(Operation {
916            parameters: vec![ReferenceOr::Reference {
917                reference: "#/components/parameters/paramA".to_string(),
918            }],
919            responses: Responses::default(),
920            ..Default::default()
921        });
922
923        spec.paths
924            .paths
925            .insert("/test".to_string(), ReferenceOr::Item(path_item));
926
927        let result = transformer.transform("test", &spec);
928        assert!(result.is_err());
929        match result.unwrap_err() {
930            crate::error::Error::Validation(msg) => {
931                assert!(
932                    msg.contains("Circular reference detected") || msg.contains("reference cycle"),
933                    "Error message should mention circular reference: {}",
934                    msg
935                );
936            }
937            _ => panic!("Expected Validation error for circular reference"),
938        }
939    }
940
941    #[test]
942    fn test_transform_with_complex_circular_reference() {
943        use openapiv3::{Components, Operation, PathItem, ReferenceOr, Responses};
944
945        let transformer = SpecTransformer::new();
946        let mut spec = create_test_spec();
947
948        let mut components = Components::default();
949
950        // Create complex circular reference: paramA -> paramB -> paramC -> paramA
951        components.parameters.insert(
952            "paramA".to_string(),
953            ReferenceOr::Reference {
954                reference: "#/components/parameters/paramB".to_string(),
955            },
956        );
957
958        components.parameters.insert(
959            "paramB".to_string(),
960            ReferenceOr::Reference {
961                reference: "#/components/parameters/paramC".to_string(),
962            },
963        );
964
965        components.parameters.insert(
966            "paramC".to_string(),
967            ReferenceOr::Reference {
968                reference: "#/components/parameters/paramA".to_string(),
969            },
970        );
971
972        spec.components = Some(components);
973
974        // Create operation with circular parameter reference
975        let mut path_item = PathItem::default();
976        path_item.get = Some(Operation {
977            parameters: vec![ReferenceOr::Reference {
978                reference: "#/components/parameters/paramA".to_string(),
979            }],
980            responses: Responses::default(),
981            ..Default::default()
982        });
983
984        spec.paths
985            .paths
986            .insert("/test".to_string(), ReferenceOr::Item(path_item));
987
988        let result = transformer.transform("test", &spec);
989        assert!(result.is_err());
990        match result.unwrap_err() {
991            crate::error::Error::Validation(msg) => {
992                assert!(
993                    msg.contains("Circular reference detected") || msg.contains("reference cycle"),
994                    "Error message should mention circular reference: {}",
995                    msg
996                );
997            }
998            _ => panic!("Expected Validation error for circular reference"),
999        }
1000    }
1001
1002    #[test]
1003    fn test_transform_with_depth_limit() {
1004        use openapiv3::{
1005            Components, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem,
1006            ReferenceOr, Responses, Schema, SchemaData, SchemaKind, Type,
1007        };
1008
1009        let transformer = SpecTransformer::new();
1010        let mut spec = create_test_spec();
1011
1012        let mut components = Components::default();
1013
1014        // Create a chain of references that exceeds MAX_REFERENCE_DEPTH
1015        for i in 0..12 {
1016            let param_name = format!("param{}", i);
1017            let next_param = format!("param{}", i + 1);
1018
1019            if i < 11 {
1020                // Reference to next parameter
1021                components.parameters.insert(
1022                    param_name,
1023                    ReferenceOr::Reference {
1024                        reference: format!("#/components/parameters/{}", next_param),
1025                    },
1026                );
1027            } else {
1028                // Last parameter is actual parameter definition
1029                let actual_param = Parameter::Path {
1030                    parameter_data: ParameterData {
1031                        name: "deepParam".to_string(),
1032                        description: Some("Very deeply nested parameter".to_string()),
1033                        required: true,
1034                        deprecated: Some(false),
1035                        format: ParameterSchemaOrContent::Schema(ReferenceOr::Item(Schema {
1036                            schema_data: SchemaData::default(),
1037                            schema_kind: SchemaKind::Type(Type::String(Default::default())),
1038                        })),
1039                        example: None,
1040                        examples: Default::default(),
1041                        explode: None,
1042                        extensions: Default::default(),
1043                    },
1044                    style: Default::default(),
1045                };
1046                components
1047                    .parameters
1048                    .insert(param_name, ReferenceOr::Item(actual_param));
1049            }
1050        }
1051
1052        spec.components = Some(components);
1053
1054        // Create operation with deeply nested parameter reference
1055        let mut path_item = PathItem::default();
1056        path_item.get = Some(Operation {
1057            parameters: vec![ReferenceOr::Reference {
1058                reference: "#/components/parameters/param0".to_string(),
1059            }],
1060            responses: Responses::default(),
1061            ..Default::default()
1062        });
1063
1064        spec.paths
1065            .paths
1066            .insert("/test".to_string(), ReferenceOr::Item(path_item));
1067
1068        let result = transformer.transform("test", &spec);
1069        assert!(result.is_err());
1070        match result.unwrap_err() {
1071            crate::error::Error::Validation(msg) => {
1072                assert!(
1073                    msg.contains("Maximum reference depth") && msg.contains("10"),
1074                    "Error message should mention depth limit: {}",
1075                    msg
1076                );
1077            }
1078            _ => panic!("Expected Validation error for depth limit"),
1079        }
1080    }
1081}