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