aperture_cli/
agent.rs

1use crate::cache::models::{
2    CachedApertureSecret, CachedCommand, CachedParameter, CachedRequestBody, CachedSpec,
3};
4use crate::config::models::GlobalConfig;
5use crate::config::url_resolver::BaseUrlResolver;
6use crate::constants;
7use crate::error::Error;
8use crate::spec::resolve_parameter_reference;
9use crate::utils::to_kebab_case;
10use openapiv3::{OpenAPI, Operation, Parameter as OpenApiParameter, ReferenceOr, SecurityScheme};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14/// JSON manifest describing all available commands and parameters for an API context.
15/// This is output when the `--describe-json` flag is used.
16#[derive(Debug, Serialize, Deserialize)]
17pub struct ApiCapabilityManifest {
18    /// Basic API metadata
19    pub api: ApiInfo,
20    /// Available command groups organized by tags
21    pub commands: HashMap<String, Vec<CommandInfo>>,
22    /// Security schemes available for this API
23    pub security_schemes: HashMap<String, SecuritySchemeInfo>,
24}
25
26#[derive(Debug, Serialize, Deserialize)]
27pub struct ApiInfo {
28    /// API name
29    pub name: String,
30    /// API version
31    pub version: String,
32    /// API description
33    pub description: Option<String>,
34    /// Base URL for the API
35    pub base_url: String,
36}
37
38#[derive(Debug, Serialize, Deserialize)]
39pub struct CommandInfo {
40    /// Command name (kebab-case operation ID)
41    pub name: String,
42    /// HTTP method
43    pub method: String,
44    /// API path with parameters
45    pub path: String,
46    /// Operation description
47    pub description: Option<String>,
48    /// Operation summary
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub summary: Option<String>,
51    /// Operation ID from the `OpenAPI` spec
52    pub operation_id: String,
53    /// Parameters for this operation
54    pub parameters: Vec<ParameterInfo>,
55    /// Request body information if applicable
56    pub request_body: Option<RequestBodyInfo>,
57    /// Security requirements for this operation
58    #[serde(skip_serializing_if = "Vec::is_empty", default)]
59    pub security_requirements: Vec<String>,
60    /// Tags associated with this operation
61    #[serde(skip_serializing_if = "Vec::is_empty", default)]
62    pub tags: Vec<String>,
63    /// Whether this operation is deprecated
64    #[serde(skip_serializing_if = "std::ops::Not::not", default)]
65    pub deprecated: bool,
66    /// External documentation URL
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub external_docs_url: Option<String>,
69}
70
71#[derive(Debug, Serialize, Deserialize)]
72pub struct ParameterInfo {
73    /// Parameter name
74    pub name: String,
75    /// Parameter location (path, query, header)
76    pub location: String,
77    /// Whether the parameter is required
78    pub required: bool,
79    /// Parameter type
80    pub param_type: String,
81    /// Parameter description
82    pub description: Option<String>,
83    /// Parameter format (e.g., int32, int64, date-time)
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub format: Option<String>,
86    /// Default value if specified
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub default_value: Option<String>,
89    /// Enumeration of valid values
90    #[serde(skip_serializing_if = "Vec::is_empty", default)]
91    pub enum_values: Vec<String>,
92    /// Example value
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub example: Option<String>,
95}
96
97#[derive(Debug, Serialize, Deserialize)]
98pub struct RequestBodyInfo {
99    /// Whether the request body is required
100    pub required: bool,
101    /// Content type (e.g., "application/json")
102    pub content_type: String,
103    /// Description of the request body
104    pub description: Option<String>,
105    /// Example of the request body
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub example: Option<String>,
108}
109
110/// Detailed, parsable security scheme description
111#[derive(Debug, Serialize, Deserialize)]
112pub struct SecuritySchemeInfo {
113    /// Type of security scheme (http, apiKey)
114    #[serde(rename = "type")]
115    pub scheme_type: String,
116    /// Optional description of the security scheme
117    pub description: Option<String>,
118    /// Detailed scheme configuration
119    #[serde(flatten)]
120    pub details: SecuritySchemeDetails,
121    /// Aperture-specific secret mapping
122    #[serde(rename = "x-aperture-secret", skip_serializing_if = "Option::is_none")]
123    pub aperture_secret: Option<CachedApertureSecret>,
124}
125
126/// Detailed security scheme configuration
127#[derive(Debug, Serialize, Deserialize)]
128#[serde(tag = "scheme", rename_all = "camelCase")]
129pub enum SecuritySchemeDetails {
130    /// HTTP authentication scheme (e.g., bearer, basic)
131    #[serde(rename = "bearer")]
132    HttpBearer {
133        /// Optional bearer token format
134        #[serde(skip_serializing_if = "Option::is_none")]
135        bearer_format: Option<String>,
136    },
137    /// HTTP basic authentication
138    #[serde(rename = "basic")]
139    HttpBasic,
140    /// API Key authentication
141    #[serde(rename = "apiKey")]
142    ApiKey {
143        /// Location of the API key (header, query, cookie)
144        #[serde(rename = "in")]
145        location: String,
146        /// Name of the parameter/header
147        name: String,
148    },
149}
150
151/// Generates a capability manifest from an `OpenAPI` specification.
152///
153/// This function creates a comprehensive JSON description of all available commands,
154/// parameters, and security requirements directly from the original `OpenAPI` spec,
155/// preserving all metadata that might be lost in the cached representation.
156///
157/// # Arguments
158/// * `api_name` - The name of the API context
159/// * `spec` - The original `OpenAPI` specification
160/// * `global_config` - Optional global configuration for URL resolution
161///
162/// # Returns
163/// * `Ok(String)` - JSON-formatted capability manifest
164/// * `Err(Error)` - If JSON serialization fails
165///
166/// # Errors
167/// Returns an error if JSON serialization fails
168pub fn generate_capability_manifest_from_openapi(
169    api_name: &str,
170    spec: &OpenAPI,
171    global_config: Option<&GlobalConfig>,
172) -> Result<String, Error> {
173    // First, convert the OpenAPI spec to a temporary CachedSpec for URL resolution
174    let base_url = spec.servers.first().map(|s| s.url.clone());
175    let servers: Vec<String> = spec.servers.iter().map(|s| s.url.clone()).collect();
176
177    let temp_cached_spec = CachedSpec {
178        cache_format_version: crate::cache::models::CACHE_FORMAT_VERSION,
179        name: api_name.to_string(),
180        version: spec.info.version.clone(),
181        commands: vec![], // We'll generate commands directly from OpenAPI
182        base_url,
183        servers,
184        security_schemes: HashMap::new(), // We'll extract these directly too
185        skipped_endpoints: vec![],        // No endpoints are skipped for agent manifest
186        server_variables: HashMap::new(), // We'll extract these later if needed
187    };
188
189    // Resolve base URL using the same priority hierarchy as executor
190    let resolver = BaseUrlResolver::new(&temp_cached_spec);
191    let resolver = if let Some(config) = global_config {
192        resolver.with_global_config(config)
193    } else {
194        resolver
195    };
196    let resolved_base_url = resolver.resolve(None);
197
198    // Extract commands directly from OpenAPI spec
199    let mut command_groups: HashMap<String, Vec<CommandInfo>> = HashMap::new();
200
201    for (path, path_item) in &spec.paths.paths {
202        if let ReferenceOr::Item(item) = path_item {
203            // Process each HTTP method
204            for (method, operation) in crate::spec::http_methods_iter(item) {
205                if let Some(op) = operation {
206                    let command_info = convert_openapi_operation_to_info(
207                        method,
208                        path,
209                        op,
210                        spec,
211                        spec.security.as_ref(),
212                    );
213
214                    // Group by first tag or "default", converted to lowercase
215                    let group_name = op
216                        .tags
217                        .first()
218                        .cloned()
219                        .unwrap_or_else(|| constants::DEFAULT_GROUP.to_string())
220                        .to_lowercase();
221
222                    command_groups
223                        .entry(group_name)
224                        .or_default()
225                        .push(command_info);
226                }
227            }
228        }
229    }
230
231    // Extract security schemes directly from OpenAPI
232    let security_schemes = extract_security_schemes_from_openapi(spec);
233
234    // Create the manifest
235    let manifest = ApiCapabilityManifest {
236        api: ApiInfo {
237            name: spec.info.title.clone(),
238            version: spec.info.version.clone(),
239            description: spec.info.description.clone(),
240            base_url: resolved_base_url,
241        },
242        commands: command_groups,
243        security_schemes,
244    };
245
246    // Serialize to JSON
247    serde_json::to_string_pretty(&manifest)
248        .map_err(|e| Error::serialization_error(format!("Failed to serialize agent manifest: {e}")))
249}
250
251/// Generates a capability manifest from a cached API specification.
252///
253/// This function creates a comprehensive JSON description of all available commands,
254/// parameters, and security requirements for the given API context.
255///
256/// # Arguments
257/// * `spec` - The cached API specification
258/// * `global_config` - Optional global configuration for URL resolution
259///
260/// # Returns
261/// * `Ok(String)` - JSON-formatted capability manifest
262/// * `Err(Error)` - If JSON serialization fails
263///
264/// # Errors
265/// Returns an error if JSON serialization fails
266pub fn generate_capability_manifest(
267    spec: &CachedSpec,
268    global_config: Option<&GlobalConfig>,
269) -> Result<String, Error> {
270    let mut command_groups: HashMap<String, Vec<CommandInfo>> = HashMap::new();
271
272    // Group commands by their tag (namespace) and convert to CommandInfo
273    for cached_command in &spec.commands {
274        let group_name = if cached_command.name.is_empty() {
275            constants::DEFAULT_GROUP.to_string()
276        } else {
277            cached_command.name.to_lowercase()
278        };
279
280        let command_info = convert_cached_command_to_info(cached_command);
281        command_groups
282            .entry(group_name)
283            .or_default()
284            .push(command_info);
285    }
286
287    // Resolve base URL using the same priority hierarchy as executor
288    let resolver = BaseUrlResolver::new(spec);
289    let resolver = if let Some(config) = global_config {
290        resolver.with_global_config(config)
291    } else {
292        resolver
293    };
294    let base_url = resolver.resolve(None);
295
296    // Create the manifest
297    let manifest = ApiCapabilityManifest {
298        api: ApiInfo {
299            name: spec.name.clone(),
300            version: spec.version.clone(),
301            description: None, // Not available in cached spec
302            base_url,
303        },
304        commands: command_groups,
305        security_schemes: extract_security_schemes(spec),
306    };
307
308    // Serialize to JSON
309    serde_json::to_string_pretty(&manifest)
310        .map_err(|e| Error::serialization_error(format!("Failed to serialize agent manifest: {e}")))
311}
312
313/// Converts a `CachedCommand` to `CommandInfo` for the manifest
314fn convert_cached_command_to_info(cached_command: &CachedCommand) -> CommandInfo {
315    let command_name = if cached_command.operation_id.is_empty() {
316        cached_command.method.to_lowercase()
317    } else {
318        to_kebab_case(&cached_command.operation_id)
319    };
320
321    let parameters: Vec<ParameterInfo> = cached_command
322        .parameters
323        .iter()
324        .map(convert_cached_parameter_to_info)
325        .collect();
326
327    let request_body = cached_command
328        .request_body
329        .as_ref()
330        .map(convert_cached_request_body_to_info);
331
332    CommandInfo {
333        name: command_name,
334        method: cached_command.method.clone(),
335        path: cached_command.path.clone(),
336        description: cached_command.description.clone(),
337        summary: cached_command.summary.clone(),
338        operation_id: cached_command.operation_id.clone(),
339        parameters,
340        request_body,
341        security_requirements: cached_command.security_requirements.clone(),
342        tags: cached_command.tags.clone(),
343        deprecated: cached_command.deprecated,
344        external_docs_url: cached_command.external_docs_url.clone(),
345    }
346}
347
348/// Converts a `CachedParameter` to `ParameterInfo` for the manifest
349fn convert_cached_parameter_to_info(cached_param: &CachedParameter) -> ParameterInfo {
350    ParameterInfo {
351        name: cached_param.name.clone(),
352        location: cached_param.location.clone(),
353        required: cached_param.required,
354        param_type: cached_param
355            .schema_type
356            .clone()
357            .unwrap_or_else(|| constants::SCHEMA_TYPE_STRING.to_string()),
358        description: cached_param.description.clone(),
359        format: cached_param.format.clone(),
360        default_value: cached_param.default_value.clone(),
361        enum_values: cached_param.enum_values.clone(),
362        example: cached_param.example.clone(),
363    }
364}
365
366/// Converts a `CachedRequestBody` to `RequestBodyInfo` for the manifest
367fn convert_cached_request_body_to_info(cached_body: &CachedRequestBody) -> RequestBodyInfo {
368    RequestBodyInfo {
369        required: cached_body.required,
370        content_type: cached_body.content_type.clone(),
371        description: cached_body.description.clone(),
372        example: cached_body.example.clone(),
373    }
374}
375
376/// Extracts security schemes from the cached spec for the capability manifest
377fn extract_security_schemes(spec: &CachedSpec) -> HashMap<String, SecuritySchemeInfo> {
378    let mut security_schemes = HashMap::new();
379
380    for (name, scheme) in &spec.security_schemes {
381        let details = match scheme.scheme_type.as_str() {
382            constants::SECURITY_TYPE_HTTP => {
383                scheme.scheme.as_ref().map_or(
384                    SecuritySchemeDetails::HttpBearer {
385                        bearer_format: None,
386                    },
387                    |http_scheme| match http_scheme.as_str() {
388                        constants::AUTH_SCHEME_BEARER => SecuritySchemeDetails::HttpBearer {
389                            bearer_format: scheme.bearer_format.clone(),
390                        },
391                        constants::AUTH_SCHEME_BASIC => SecuritySchemeDetails::HttpBasic,
392                        _ => {
393                            // For other HTTP schemes, default to bearer
394                            SecuritySchemeDetails::HttpBearer {
395                                bearer_format: None,
396                            }
397                        }
398                    },
399                )
400            }
401            constants::AUTH_SCHEME_APIKEY => SecuritySchemeDetails::ApiKey {
402                location: scheme
403                    .location
404                    .clone()
405                    .unwrap_or_else(|| constants::LOCATION_HEADER.to_string()),
406                name: scheme
407                    .parameter_name
408                    .clone()
409                    .unwrap_or_else(|| constants::HEADER_AUTHORIZATION.to_string()),
410            },
411            _ => {
412                // Default to bearer for unknown types
413                SecuritySchemeDetails::HttpBearer {
414                    bearer_format: None,
415                }
416            }
417        };
418
419        let scheme_info = SecuritySchemeInfo {
420            scheme_type: scheme.scheme_type.clone(),
421            description: scheme.description.clone(),
422            details,
423            aperture_secret: scheme.aperture_secret.clone(),
424        };
425
426        security_schemes.insert(name.clone(), scheme_info);
427    }
428
429    security_schemes
430}
431
432/// Converts an `OpenAPI` operation to `CommandInfo` with full metadata
433fn convert_openapi_operation_to_info(
434    method: &str,
435    path: &str,
436    operation: &Operation,
437    spec: &OpenAPI,
438    global_security: Option<&Vec<openapiv3::SecurityRequirement>>,
439) -> CommandInfo {
440    let command_name = operation
441        .operation_id
442        .as_ref()
443        .map_or_else(|| method.to_lowercase(), |op_id| to_kebab_case(op_id));
444
445    // Extract parameters with full metadata, resolving references
446    let parameters: Vec<ParameterInfo> = operation
447        .parameters
448        .iter()
449        .filter_map(|param_ref| match param_ref {
450            ReferenceOr::Item(param) => Some(convert_openapi_parameter_to_info(param)),
451            ReferenceOr::Reference { reference } => resolve_parameter_reference(spec, reference)
452                .ok()
453                .map(|param| convert_openapi_parameter_to_info(&param)),
454        })
455        .collect();
456
457    // Extract request body info
458    let request_body = operation.request_body.as_ref().and_then(|rb_ref| {
459        if let ReferenceOr::Item(body) = rb_ref {
460            // Prefer JSON content if available
461            let content_type = if body.content.contains_key(constants::CONTENT_TYPE_JSON) {
462                constants::CONTENT_TYPE_JSON
463            } else {
464                body.content.keys().next().map(String::as_str)?
465            };
466
467            let media_type = body.content.get(content_type)?;
468            let example = media_type
469                .example
470                .as_ref()
471                .map(|ex| serde_json::to_string(ex).unwrap_or_else(|_| ex.to_string()));
472
473            Some(RequestBodyInfo {
474                required: body.required,
475                content_type: content_type.to_string(),
476                description: body.description.clone(),
477                example,
478            })
479        } else {
480            None
481        }
482    });
483
484    // Extract security requirements
485    let security_requirements = operation.security.as_ref().map_or_else(
486        || {
487            global_security.map_or(vec![], |reqs| {
488                reqs.iter().flat_map(|req| req.keys().cloned()).collect()
489            })
490        },
491        |op_security| {
492            op_security
493                .iter()
494                .flat_map(|req| req.keys().cloned())
495                .collect()
496        },
497    );
498
499    CommandInfo {
500        name: command_name,
501        method: method.to_uppercase(),
502        path: path.to_string(),
503        description: operation.description.clone(),
504        summary: operation.summary.clone(),
505        operation_id: operation.operation_id.clone().unwrap_or_default(),
506        parameters,
507        request_body,
508        security_requirements,
509        tags: operation.tags.clone(),
510        deprecated: operation.deprecated,
511        external_docs_url: operation
512            .external_docs
513            .as_ref()
514            .map(|docs| docs.url.clone()),
515    }
516}
517
518/// Converts an `OpenAPI` parameter to `ParameterInfo` with full metadata
519fn convert_openapi_parameter_to_info(param: &OpenApiParameter) -> ParameterInfo {
520    let (param_data, location_str) = match param {
521        OpenApiParameter::Query { parameter_data, .. } => {
522            (parameter_data, constants::PARAM_LOCATION_QUERY)
523        }
524        OpenApiParameter::Header { parameter_data, .. } => {
525            (parameter_data, constants::PARAM_LOCATION_HEADER)
526        }
527        OpenApiParameter::Path { parameter_data, .. } => {
528            (parameter_data, constants::PARAM_LOCATION_PATH)
529        }
530        OpenApiParameter::Cookie { parameter_data, .. } => {
531            (parameter_data, constants::PARAM_LOCATION_COOKIE)
532        }
533    };
534
535    // Extract schema information
536    let (schema_type, format, default_value, enum_values, example) =
537        if let openapiv3::ParameterSchemaOrContent::Schema(schema_ref) = &param_data.format {
538            match schema_ref {
539                ReferenceOr::Item(schema) => {
540                    let (schema_type, format, enums) = match &schema.schema_kind {
541                        openapiv3::SchemaKind::Type(type_val) => match type_val {
542                            openapiv3::Type::String(string_type) => {
543                                let enum_values: Vec<String> = string_type
544                                    .enumeration
545                                    .iter()
546                                    .filter_map(|v| v.as_ref())
547                                    .map(|v| {
548                                        serde_json::to_string(v).unwrap_or_else(|_| v.to_string())
549                                    })
550                                    .collect();
551                                (constants::SCHEMA_TYPE_STRING.to_string(), None, enum_values)
552                            }
553                            openapiv3::Type::Number(_) => {
554                                (constants::SCHEMA_TYPE_NUMBER.to_string(), None, vec![])
555                            }
556                            openapiv3::Type::Integer(_) => {
557                                (constants::SCHEMA_TYPE_INTEGER.to_string(), None, vec![])
558                            }
559                            openapiv3::Type::Boolean(_) => {
560                                (constants::SCHEMA_TYPE_BOOLEAN.to_string(), None, vec![])
561                            }
562                            openapiv3::Type::Array(_) => {
563                                (constants::SCHEMA_TYPE_ARRAY.to_string(), None, vec![])
564                            }
565                            openapiv3::Type::Object(_) => {
566                                (constants::SCHEMA_TYPE_OBJECT.to_string(), None, vec![])
567                            }
568                        },
569                        _ => (constants::SCHEMA_TYPE_STRING.to_string(), None, vec![]),
570                    };
571
572                    let default_value = schema
573                        .schema_data
574                        .default
575                        .as_ref()
576                        .map(|v| serde_json::to_string(v).unwrap_or_else(|_| v.to_string()));
577
578                    (Some(schema_type), format, default_value, enums, None)
579                }
580                ReferenceOr::Reference { .. } => (
581                    Some(constants::SCHEMA_TYPE_STRING.to_string()),
582                    None,
583                    None,
584                    vec![],
585                    None,
586                ),
587            }
588        } else {
589            (
590                Some(constants::SCHEMA_TYPE_STRING.to_string()),
591                None,
592                None,
593                vec![],
594                None,
595            )
596        };
597
598    // Extract example from parameter data
599    let example = param_data
600        .example
601        .as_ref()
602        .map(|ex| serde_json::to_string(ex).unwrap_or_else(|_| ex.to_string()))
603        .or(example);
604
605    ParameterInfo {
606        name: param_data.name.clone(),
607        location: location_str.to_string(),
608        required: param_data.required,
609        param_type: schema_type.unwrap_or_else(|| constants::SCHEMA_TYPE_STRING.to_string()),
610        description: param_data.description.clone(),
611        format,
612        default_value,
613        enum_values,
614        example,
615    }
616}
617
618/// Extracts security schemes directly from `OpenAPI` spec
619fn extract_security_schemes_from_openapi(spec: &OpenAPI) -> HashMap<String, SecuritySchemeInfo> {
620    let mut security_schemes = HashMap::new();
621
622    if let Some(components) = &spec.components {
623        for (name, scheme_ref) in &components.security_schemes {
624            if let ReferenceOr::Item(scheme) = scheme_ref {
625                if let Some(scheme_info) = convert_openapi_security_scheme(name, scheme) {
626                    security_schemes.insert(name.clone(), scheme_info);
627                }
628            }
629        }
630    }
631
632    security_schemes
633}
634
635/// Converts an `OpenAPI` security scheme to `SecuritySchemeInfo`
636fn convert_openapi_security_scheme(
637    _name: &str,
638    scheme: &SecurityScheme,
639) -> Option<SecuritySchemeInfo> {
640    match scheme {
641        SecurityScheme::APIKey {
642            location,
643            name: param_name,
644            description,
645            ..
646        } => {
647            let location_str = match location {
648                openapiv3::APIKeyLocation::Query => constants::PARAM_LOCATION_QUERY,
649                openapiv3::APIKeyLocation::Header => constants::PARAM_LOCATION_HEADER,
650                openapiv3::APIKeyLocation::Cookie => constants::PARAM_LOCATION_COOKIE,
651            };
652
653            let aperture_secret = extract_aperture_secret_from_extensions(scheme);
654
655            Some(SecuritySchemeInfo {
656                scheme_type: constants::AUTH_SCHEME_APIKEY.to_string(),
657                description: description.clone(),
658                details: SecuritySchemeDetails::ApiKey {
659                    location: location_str.to_string(),
660                    name: param_name.clone(),
661                },
662                aperture_secret,
663            })
664        }
665        SecurityScheme::HTTP {
666            scheme: http_scheme,
667            bearer_format,
668            description,
669            ..
670        } => {
671            let details = match http_scheme.as_str() {
672                constants::AUTH_SCHEME_BEARER => SecuritySchemeDetails::HttpBearer {
673                    bearer_format: bearer_format.clone(),
674                },
675                constants::AUTH_SCHEME_BASIC => SecuritySchemeDetails::HttpBasic,
676                _ => SecuritySchemeDetails::HttpBearer {
677                    bearer_format: None,
678                },
679            };
680
681            let aperture_secret = extract_aperture_secret_from_extensions(scheme);
682
683            Some(SecuritySchemeInfo {
684                scheme_type: constants::SECURITY_TYPE_HTTP.to_string(),
685                description: description.clone(),
686                details,
687                aperture_secret,
688            })
689        }
690        SecurityScheme::OAuth2 { .. } | SecurityScheme::OpenIDConnect { .. } => None,
691    }
692}
693
694/// Extracts x-aperture-secret extension from a security scheme's extensions
695fn extract_aperture_secret_from_extensions(
696    scheme: &SecurityScheme,
697) -> Option<CachedApertureSecret> {
698    let extensions = match scheme {
699        SecurityScheme::APIKey { extensions, .. } | SecurityScheme::HTTP { extensions, .. } => {
700            extensions
701        }
702        SecurityScheme::OAuth2 { .. } | SecurityScheme::OpenIDConnect { .. } => return None,
703    };
704
705    extensions
706        .get(constants::EXT_APERTURE_SECRET)
707        .and_then(|value| {
708            if let Some(obj) = value.as_object() {
709                let source = obj.get(constants::EXT_KEY_SOURCE)?.as_str()?;
710                let name = obj.get(constants::EXT_KEY_NAME)?.as_str()?;
711
712                if source == constants::SOURCE_ENV {
713                    return Some(CachedApertureSecret {
714                        source: source.to_string(),
715                        name: name.to_string(),
716                    });
717                }
718            }
719            None
720        })
721}
722
723#[cfg(test)]
724mod tests {
725    use super::*;
726    use crate::cache::models::{CachedCommand, CachedParameter, CachedSpec};
727
728    #[test]
729    fn test_command_name_conversion() {
730        // Test that command names are properly converted
731        assert_eq!(to_kebab_case("getUserById"), "get-user-by-id");
732        assert_eq!(to_kebab_case("createUser"), "create-user");
733        assert_eq!(to_kebab_case("list"), "list");
734        assert_eq!(to_kebab_case("GET"), "get");
735        assert_eq!(
736            to_kebab_case("List an Organization's Issues"),
737            "list-an-organizations-issues"
738        );
739    }
740
741    #[test]
742    fn test_generate_capability_manifest() {
743        use crate::cache::models::{CachedApertureSecret, CachedSecurityScheme};
744
745        let mut security_schemes = HashMap::new();
746        security_schemes.insert(
747            "bearerAuth".to_string(),
748            CachedSecurityScheme {
749                name: "bearerAuth".to_string(),
750                scheme_type: constants::SECURITY_TYPE_HTTP.to_string(),
751                scheme: Some(constants::AUTH_SCHEME_BEARER.to_string()),
752                location: Some(constants::LOCATION_HEADER.to_string()),
753                parameter_name: Some(constants::HEADER_AUTHORIZATION.to_string()),
754                description: None,
755                bearer_format: None,
756                aperture_secret: Some(CachedApertureSecret {
757                    source: constants::SOURCE_ENV.to_string(),
758                    name: "API_TOKEN".to_string(),
759                }),
760            },
761        );
762
763        let spec = CachedSpec {
764            cache_format_version: crate::cache::models::CACHE_FORMAT_VERSION,
765            name: "Test API".to_string(),
766            version: "1.0.0".to_string(),
767            commands: vec![CachedCommand {
768                name: "users".to_string(),
769                description: Some("Get user by ID".to_string()),
770                summary: None,
771                operation_id: "getUserById".to_string(),
772                method: constants::HTTP_METHOD_GET.to_string(),
773                path: "/users/{id}".to_string(),
774                parameters: vec![CachedParameter {
775                    name: "id".to_string(),
776                    location: constants::PARAM_LOCATION_PATH.to_string(),
777                    required: true,
778                    description: None,
779                    schema: Some(constants::SCHEMA_TYPE_STRING.to_string()),
780                    schema_type: Some(constants::SCHEMA_TYPE_STRING.to_string()),
781                    format: None,
782                    default_value: None,
783                    enum_values: vec![],
784                    example: None,
785                }],
786                request_body: None,
787                responses: vec![],
788                security_requirements: vec!["bearerAuth".to_string()],
789                tags: vec!["users".to_string()],
790                deprecated: false,
791                external_docs_url: None,
792            }],
793            base_url: Some("https://test-api.example.com".to_string()),
794            servers: vec!["https://test-api.example.com".to_string()],
795            security_schemes,
796            skipped_endpoints: vec![],
797            server_variables: HashMap::new(),
798        };
799
800        let manifest_json = generate_capability_manifest(&spec, None).unwrap();
801        let manifest: ApiCapabilityManifest = serde_json::from_str(&manifest_json).unwrap();
802
803        assert_eq!(manifest.api.name, "Test API");
804        assert_eq!(manifest.api.version, "1.0.0");
805        assert!(manifest.commands.contains_key("users"));
806
807        let users_commands = &manifest.commands["users"];
808        assert_eq!(users_commands.len(), 1);
809        assert_eq!(users_commands[0].name, "get-user-by-id");
810        assert_eq!(users_commands[0].method, constants::HTTP_METHOD_GET);
811        assert_eq!(users_commands[0].parameters.len(), 1);
812        assert_eq!(users_commands[0].parameters[0].name, "id");
813
814        // Test security information extraction
815        assert!(!manifest.security_schemes.is_empty());
816        assert!(manifest.security_schemes.contains_key("bearerAuth"));
817        let bearer_auth = &manifest.security_schemes["bearerAuth"];
818        assert_eq!(bearer_auth.scheme_type, constants::SECURITY_TYPE_HTTP);
819        assert!(matches!(
820            &bearer_auth.details,
821            SecuritySchemeDetails::HttpBearer { .. }
822        ));
823        assert!(bearer_auth.aperture_secret.is_some());
824        let aperture_secret = bearer_auth.aperture_secret.as_ref().unwrap();
825        assert_eq!(aperture_secret.name, "API_TOKEN");
826        assert_eq!(aperture_secret.source, constants::SOURCE_ENV);
827    }
828}