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