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