aperture_cli/spec/
transformer.rs

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