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, resolve_schema_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
14type ParameterSchemaInfo = (
17 Option<String>,
18 Option<String>,
19 Option<String>,
20 Vec<String>,
21 Option<String>,
22);
23
24#[derive(Debug, Serialize, Deserialize)]
27pub struct ApiCapabilityManifest {
28 pub api: ApiInfo,
30 pub endpoints: EndpointStatistics,
32 pub commands: HashMap<String, Vec<CommandInfo>>,
34 pub security_schemes: HashMap<String, SecuritySchemeInfo>,
36}
37
38#[derive(Debug, Serialize, Deserialize)]
39pub struct ApiInfo {
40 pub name: String,
42 pub version: String,
44 pub description: Option<String>,
46 pub base_url: String,
48}
49
50#[derive(Debug, Serialize, Deserialize)]
52pub struct EndpointStatistics {
53 pub total: usize,
55 pub available: usize,
57 pub skipped: usize,
59}
60
61#[derive(Debug, Serialize, Deserialize)]
62pub struct CommandInfo {
63 pub name: String,
65 pub method: String,
67 pub path: String,
69 pub description: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub summary: Option<String>,
74 pub operation_id: String,
76 pub parameters: Vec<ParameterInfo>,
78 pub request_body: Option<RequestBodyInfo>,
80 #[serde(skip_serializing_if = "Vec::is_empty", default)]
82 pub security_requirements: Vec<String>,
83 #[serde(skip_serializing_if = "Vec::is_empty", default)]
85 pub tags: Vec<String>,
86 #[serde(skip_serializing_if = "Vec::is_empty", default)]
88 pub original_tags: Vec<String>,
89 #[serde(skip_serializing_if = "std::ops::Not::not", default)]
91 pub deprecated: bool,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub external_docs_url: Option<String>,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub response_schema: Option<ResponseSchemaInfo>,
98}
99
100#[derive(Debug, Serialize, Deserialize)]
101pub struct ParameterInfo {
102 pub name: String,
104 pub location: String,
106 pub required: bool,
108 pub param_type: String,
110 pub description: Option<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub format: Option<String>,
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub default_value: Option<String>,
118 #[serde(skip_serializing_if = "Vec::is_empty", default)]
120 pub enum_values: Vec<String>,
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub example: Option<String>,
124}
125
126#[derive(Debug, Serialize, Deserialize)]
127pub struct RequestBodyInfo {
128 pub required: bool,
130 pub content_type: String,
132 pub description: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub example: Option<String>,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct ResponseSchemaInfo {
163 pub content_type: String,
165 pub schema: serde_json::Value,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub example: Option<serde_json::Value>,
173}
174
175#[derive(Debug, Serialize, Deserialize)]
177pub struct SecuritySchemeInfo {
178 #[serde(rename = "type")]
180 pub scheme_type: String,
181 pub description: Option<String>,
183 #[serde(flatten)]
185 pub details: SecuritySchemeDetails,
186 #[serde(rename = "x-aperture-secret", skip_serializing_if = "Option::is_none")]
188 pub aperture_secret: Option<CachedApertureSecret>,
189}
190
191#[derive(Debug, Serialize, Deserialize)]
193#[serde(tag = "scheme", rename_all = "camelCase")]
194pub enum SecuritySchemeDetails {
195 #[serde(rename = "bearer")]
197 HttpBearer {
198 #[serde(skip_serializing_if = "Option::is_none")]
200 bearer_format: Option<String>,
201 },
202 #[serde(rename = "basic")]
204 HttpBasic,
205 #[serde(rename = "apiKey")]
207 ApiKey {
208 #[serde(rename = "in")]
210 location: String,
211 name: String,
213 },
214}
215
216pub fn generate_capability_manifest_from_openapi(
234 api_name: &str,
235 spec: &OpenAPI,
236 cached_spec: &CachedSpec,
237 global_config: Option<&GlobalConfig>,
238) -> Result<String, Error> {
239 let base_url = spec.servers.first().map(|s| s.url.clone());
241 let servers: Vec<String> = spec.servers.iter().map(|s| s.url.clone()).collect();
242
243 let temp_cached_spec = CachedSpec {
244 cache_format_version: crate::cache::models::CACHE_FORMAT_VERSION,
245 name: api_name.to_string(),
246 version: spec.info.version.clone(),
247 commands: vec![], base_url,
249 servers,
250 security_schemes: HashMap::new(), skipped_endpoints: vec![], server_variables: HashMap::new(), };
254
255 let resolver = BaseUrlResolver::new(&temp_cached_spec);
257 let resolver = if let Some(config) = global_config {
258 resolver.with_global_config(config)
259 } else {
260 resolver
261 };
262 let resolved_base_url = resolver.resolve(None);
263
264 let mut command_groups: HashMap<String, Vec<CommandInfo>> = HashMap::new();
266
267 let skipped_set: std::collections::HashSet<(&str, &str)> = cached_spec
269 .skipped_endpoints
270 .iter()
271 .map(|ep| (ep.path.as_str(), ep.method.as_str()))
272 .collect();
273
274 for (path, path_item) in &spec.paths.paths {
275 let ReferenceOr::Item(item) = path_item else {
276 continue;
277 };
278
279 for (method, operation) in crate::spec::http_methods_iter(item) {
281 let Some(op) = operation else {
282 continue;
283 };
284
285 if skipped_set.contains(&(path.as_str(), method.to_uppercase().as_str())) {
287 continue;
288 }
289
290 let command_info =
291 convert_openapi_operation_to_info(method, path, op, spec, spec.security.as_ref());
292
293 let group_name = op.tags.first().map_or_else(
295 || constants::DEFAULT_GROUP.to_string(),
296 |tag| to_kebab_case(tag),
297 );
298
299 command_groups
300 .entry(group_name)
301 .or_default()
302 .push(command_info);
303 }
304 }
305
306 let security_schemes = extract_security_schemes_from_openapi(spec);
308
309 let skipped = cached_spec.skipped_endpoints.len();
311 let available = cached_spec.commands.len();
312 let total = available + skipped;
313
314 let manifest = ApiCapabilityManifest {
316 api: ApiInfo {
317 name: spec.info.title.clone(),
318 version: spec.info.version.clone(),
319 description: spec.info.description.clone(),
320 base_url: resolved_base_url,
321 },
322 endpoints: EndpointStatistics {
323 total,
324 available,
325 skipped,
326 },
327 commands: command_groups,
328 security_schemes,
329 };
330
331 serde_json::to_string_pretty(&manifest)
333 .map_err(|e| Error::serialization_error(format!("Failed to serialize agent manifest: {e}")))
334}
335
336pub fn generate_capability_manifest(
352 spec: &CachedSpec,
353 global_config: Option<&GlobalConfig>,
354) -> Result<String, Error> {
355 let mut command_groups: HashMap<String, Vec<CommandInfo>> = HashMap::new();
356
357 for cached_command in &spec.commands {
359 let group_name = if cached_command.name.is_empty() {
360 constants::DEFAULT_GROUP.to_string()
361 } else {
362 to_kebab_case(&cached_command.name)
363 };
364
365 let command_info = convert_cached_command_to_info(cached_command);
366 command_groups
367 .entry(group_name)
368 .or_default()
369 .push(command_info);
370 }
371
372 let resolver = BaseUrlResolver::new(spec);
374 let resolver = if let Some(config) = global_config {
375 resolver.with_global_config(config)
376 } else {
377 resolver
378 };
379 let base_url = resolver.resolve(None);
380
381 let skipped = spec.skipped_endpoints.len();
383 let available = spec.commands.len();
384 let total = available + skipped;
385
386 let manifest = ApiCapabilityManifest {
388 api: ApiInfo {
389 name: spec.name.clone(),
390 version: spec.version.clone(),
391 description: None, base_url,
393 },
394 endpoints: EndpointStatistics {
395 total,
396 available,
397 skipped,
398 },
399 commands: command_groups,
400 security_schemes: extract_security_schemes(spec),
401 };
402
403 serde_json::to_string_pretty(&manifest)
405 .map_err(|e| Error::serialization_error(format!("Failed to serialize agent manifest: {e}")))
406}
407
408fn convert_cached_command_to_info(cached_command: &CachedCommand) -> CommandInfo {
410 let command_name = if cached_command.operation_id.is_empty() {
411 cached_command.method.to_lowercase()
412 } else {
413 to_kebab_case(&cached_command.operation_id)
414 };
415
416 let parameters: Vec<ParameterInfo> = cached_command
417 .parameters
418 .iter()
419 .map(convert_cached_parameter_to_info)
420 .collect();
421
422 let request_body = cached_command
423 .request_body
424 .as_ref()
425 .map(convert_cached_request_body_to_info);
426
427 let response_schema = extract_response_schema_from_cached(&cached_command.responses);
429
430 CommandInfo {
431 name: command_name,
432 method: cached_command.method.clone(),
433 path: cached_command.path.clone(),
434 description: cached_command.description.clone(),
435 summary: cached_command.summary.clone(),
436 operation_id: cached_command.operation_id.clone(),
437 parameters,
438 request_body,
439 security_requirements: cached_command.security_requirements.clone(),
440 tags: cached_command
441 .tags
442 .iter()
443 .map(|t| to_kebab_case(t))
444 .collect(),
445 original_tags: cached_command.tags.clone(),
446 deprecated: cached_command.deprecated,
447 external_docs_url: cached_command.external_docs_url.clone(),
448 response_schema,
449 }
450}
451
452fn convert_cached_parameter_to_info(cached_param: &CachedParameter) -> ParameterInfo {
454 ParameterInfo {
455 name: cached_param.name.clone(),
456 location: cached_param.location.clone(),
457 required: cached_param.required,
458 param_type: cached_param
459 .schema_type
460 .clone()
461 .unwrap_or_else(|| constants::SCHEMA_TYPE_STRING.to_string()),
462 description: cached_param.description.clone(),
463 format: cached_param.format.clone(),
464 default_value: cached_param.default_value.clone(),
465 enum_values: cached_param.enum_values.clone(),
466 example: cached_param.example.clone(),
467 }
468}
469
470fn convert_cached_request_body_to_info(cached_body: &CachedRequestBody) -> RequestBodyInfo {
472 RequestBodyInfo {
473 required: cached_body.required,
474 content_type: cached_body.content_type.clone(),
475 description: cached_body.description.clone(),
476 example: cached_body.example.clone(),
477 }
478}
479
480fn extract_response_schema_from_cached(
486 responses: &[crate::cache::models::CachedResponse],
487) -> Option<ResponseSchemaInfo> {
488 constants::SUCCESS_STATUS_CODES.iter().find_map(|code| {
489 responses
490 .iter()
491 .find(|r| r.status_code == *code)
492 .and_then(|response| {
493 let content_type = response.content_type.as_ref()?;
494 let schema_str = response.schema.as_ref()?;
495 let schema = serde_json::from_str(schema_str).ok()?;
496 let example = response
497 .example
498 .as_ref()
499 .and_then(|ex| serde_json::from_str(ex).ok());
500
501 Some(ResponseSchemaInfo {
502 content_type: content_type.clone(),
503 schema,
504 example,
505 })
506 })
507 })
508}
509
510fn extract_security_schemes(spec: &CachedSpec) -> HashMap<String, SecuritySchemeInfo> {
512 let mut security_schemes = HashMap::new();
513
514 for (name, scheme) in &spec.security_schemes {
515 let details = match scheme.scheme_type.as_str() {
516 constants::SECURITY_TYPE_HTTP => {
517 scheme.scheme.as_ref().map_or(
518 SecuritySchemeDetails::HttpBearer {
519 bearer_format: None,
520 },
521 |http_scheme| match http_scheme.as_str() {
522 constants::AUTH_SCHEME_BEARER => SecuritySchemeDetails::HttpBearer {
523 bearer_format: scheme.bearer_format.clone(),
524 },
525 constants::AUTH_SCHEME_BASIC => SecuritySchemeDetails::HttpBasic,
526 _ => {
527 SecuritySchemeDetails::HttpBearer {
529 bearer_format: None,
530 }
531 }
532 },
533 )
534 }
535 constants::AUTH_SCHEME_APIKEY => SecuritySchemeDetails::ApiKey {
536 location: scheme
537 .location
538 .clone()
539 .unwrap_or_else(|| constants::LOCATION_HEADER.to_string()),
540 name: scheme
541 .parameter_name
542 .clone()
543 .unwrap_or_else(|| constants::HEADER_AUTHORIZATION.to_string()),
544 },
545 _ => {
546 SecuritySchemeDetails::HttpBearer {
548 bearer_format: None,
549 }
550 }
551 };
552
553 let scheme_info = SecuritySchemeInfo {
554 scheme_type: scheme.scheme_type.clone(),
555 description: scheme.description.clone(),
556 details,
557 aperture_secret: scheme.aperture_secret.clone(),
558 };
559
560 security_schemes.insert(name.clone(), scheme_info);
561 }
562
563 security_schemes
564}
565
566fn convert_openapi_operation_to_info(
568 method: &str,
569 path: &str,
570 operation: &Operation,
571 spec: &OpenAPI,
572 global_security: Option<&Vec<openapiv3::SecurityRequirement>>,
573) -> CommandInfo {
574 let command_name = operation
575 .operation_id
576 .as_ref()
577 .map_or_else(|| method.to_lowercase(), |op_id| to_kebab_case(op_id));
578
579 let parameters: Vec<ParameterInfo> = operation
581 .parameters
582 .iter()
583 .filter_map(|param_ref| match param_ref {
584 ReferenceOr::Item(param) => Some(convert_openapi_parameter_to_info(param)),
585 ReferenceOr::Reference { reference } => resolve_parameter_reference(spec, reference)
586 .ok()
587 .map(|param| convert_openapi_parameter_to_info(¶m)),
588 })
589 .collect();
590
591 let request_body = operation.request_body.as_ref().and_then(|rb_ref| {
593 let ReferenceOr::Item(body) = rb_ref else {
594 return None;
595 };
596
597 let content_type = if body.content.contains_key(constants::CONTENT_TYPE_JSON) {
599 constants::CONTENT_TYPE_JSON
600 } else {
601 body.content.keys().next().map(String::as_str)?
602 };
603
604 let media_type = body.content.get(content_type)?;
605 let example = media_type
606 .example
607 .as_ref()
608 .map(|ex| serde_json::to_string(ex).unwrap_or_else(|_| ex.to_string()));
609
610 Some(RequestBodyInfo {
611 required: body.required,
612 content_type: content_type.to_string(),
613 description: body.description.clone(),
614 example,
615 })
616 });
617
618 let security_requirements = operation.security.as_ref().map_or_else(
620 || {
621 global_security.map_or(vec![], |reqs| {
622 reqs.iter().flat_map(|req| req.keys().cloned()).collect()
623 })
624 },
625 |op_security| {
626 op_security
627 .iter()
628 .flat_map(|req| req.keys().cloned())
629 .collect()
630 },
631 );
632
633 let response_schema = extract_response_schema_from_operation(operation, spec);
635
636 CommandInfo {
637 name: command_name,
638 method: method.to_uppercase(),
639 path: path.to_string(),
640 description: operation.description.clone(),
641 summary: operation.summary.clone(),
642 operation_id: operation.operation_id.clone().unwrap_or_default(),
643 parameters,
644 request_body,
645 security_requirements,
646 tags: operation.tags.iter().map(|t| to_kebab_case(t)).collect(),
647 original_tags: operation.tags.clone(),
648 deprecated: operation.deprecated,
649 external_docs_url: operation
650 .external_docs
651 .as_ref()
652 .map(|docs| docs.url.clone()),
653 response_schema,
654 }
655}
656
657fn extract_response_schema_from_operation(
662 operation: &Operation,
663 spec: &OpenAPI,
664) -> Option<ResponseSchemaInfo> {
665 constants::SUCCESS_STATUS_CODES.iter().find_map(|code| {
666 operation
667 .responses
668 .responses
669 .get(&openapiv3::StatusCode::Code(
670 code.parse().expect("valid status code"),
671 ))
672 .and_then(|response_ref| extract_response_schema_from_response(response_ref, spec))
673 })
674}
675
676fn extract_response_schema_from_response(
689 response_ref: &ReferenceOr<openapiv3::Response>,
690 spec: &OpenAPI,
691) -> Option<ResponseSchemaInfo> {
692 let ReferenceOr::Item(response) = response_ref else {
696 return None;
697 };
698
699 let content_type = if response.content.contains_key(constants::CONTENT_TYPE_JSON) {
701 constants::CONTENT_TYPE_JSON
702 } else {
703 response.content.keys().next().map(String::as_str)?
705 };
706
707 let media_type = response.content.get(content_type)?;
708 let schema_ref = media_type.schema.as_ref()?;
709
710 let schema_value = match schema_ref {
712 ReferenceOr::Item(schema) => serde_json::to_value(schema).ok()?,
713 ReferenceOr::Reference { reference } => {
714 let resolved = resolve_schema_reference(spec, reference).ok()?;
715 serde_json::to_value(&resolved).ok()?
716 }
717 };
718
719 let example = media_type
721 .example
722 .as_ref()
723 .and_then(|ex| serde_json::to_value(ex).ok());
724
725 Some(ResponseSchemaInfo {
726 content_type: content_type.to_string(),
727 schema: schema_value,
728 example,
729 })
730}
731
732fn extract_schema_info_from_parameter(
734 format: &openapiv3::ParameterSchemaOrContent,
735) -> ParameterSchemaInfo {
736 let openapiv3::ParameterSchemaOrContent::Schema(schema_ref) = format else {
737 return (
738 Some(constants::SCHEMA_TYPE_STRING.to_string()),
739 None,
740 None,
741 vec![],
742 None,
743 );
744 };
745
746 match schema_ref {
747 ReferenceOr::Item(schema) => {
748 let (schema_type, format, enums) =
749 extract_schema_type_from_schema_kind(&schema.schema_kind);
750
751 let default_value = schema
752 .schema_data
753 .default
754 .as_ref()
755 .map(|v| serde_json::to_string(v).unwrap_or_else(|_| v.to_string()));
756
757 (Some(schema_type), format, default_value, enums, None)
758 }
759 ReferenceOr::Reference { .. } => (
760 Some(constants::SCHEMA_TYPE_STRING.to_string()),
761 None,
762 None,
763 vec![],
764 None,
765 ),
766 }
767}
768
769fn extract_schema_type_from_schema_kind(
771 schema_kind: &openapiv3::SchemaKind,
772) -> (String, Option<String>, Vec<String>) {
773 match schema_kind {
774 openapiv3::SchemaKind::Type(type_val) => match type_val {
775 openapiv3::Type::String(string_type) => {
776 let enum_values: Vec<String> = string_type
777 .enumeration
778 .iter()
779 .filter_map(|v| v.as_ref())
780 .map(|v| serde_json::to_string(v).unwrap_or_else(|_| v.clone()))
781 .collect();
782 (constants::SCHEMA_TYPE_STRING.to_string(), None, enum_values)
783 }
784 openapiv3::Type::Number(_) => (constants::SCHEMA_TYPE_NUMBER.to_string(), None, vec![]),
785 openapiv3::Type::Integer(_) => {
786 (constants::SCHEMA_TYPE_INTEGER.to_string(), None, vec![])
787 }
788 openapiv3::Type::Boolean(_) => {
789 (constants::SCHEMA_TYPE_BOOLEAN.to_string(), None, vec![])
790 }
791 openapiv3::Type::Array(_) => (constants::SCHEMA_TYPE_ARRAY.to_string(), None, vec![]),
792 openapiv3::Type::Object(_) => (constants::SCHEMA_TYPE_OBJECT.to_string(), None, vec![]),
793 },
794 _ => (constants::SCHEMA_TYPE_STRING.to_string(), None, vec![]),
795 }
796}
797
798fn convert_openapi_parameter_to_info(param: &OpenApiParameter) -> ParameterInfo {
800 let (param_data, location_str) = match param {
801 OpenApiParameter::Query { parameter_data, .. } => {
802 (parameter_data, constants::PARAM_LOCATION_QUERY)
803 }
804 OpenApiParameter::Header { parameter_data, .. } => {
805 (parameter_data, constants::PARAM_LOCATION_HEADER)
806 }
807 OpenApiParameter::Path { parameter_data, .. } => {
808 (parameter_data, constants::PARAM_LOCATION_PATH)
809 }
810 OpenApiParameter::Cookie { parameter_data, .. } => {
811 (parameter_data, constants::PARAM_LOCATION_COOKIE)
812 }
813 };
814
815 let (schema_type, format, default_value, enum_values, example) =
817 extract_schema_info_from_parameter(¶m_data.format);
818
819 let example = param_data
821 .example
822 .as_ref()
823 .map(|ex| serde_json::to_string(ex).unwrap_or_else(|_| ex.to_string()))
824 .or(example);
825
826 ParameterInfo {
827 name: param_data.name.clone(),
828 location: location_str.to_string(),
829 required: param_data.required,
830 param_type: schema_type.unwrap_or_else(|| constants::SCHEMA_TYPE_STRING.to_string()),
831 description: param_data.description.clone(),
832 format,
833 default_value,
834 enum_values,
835 example,
836 }
837}
838
839fn extract_security_schemes_from_openapi(spec: &OpenAPI) -> HashMap<String, SecuritySchemeInfo> {
841 let mut security_schemes = HashMap::new();
842
843 let Some(components) = &spec.components else {
844 return security_schemes;
845 };
846
847 for (name, scheme_ref) in &components.security_schemes {
848 let ReferenceOr::Item(scheme) = scheme_ref else {
849 continue;
850 };
851
852 let Some(scheme_info) = convert_openapi_security_scheme(name, scheme) else {
853 continue;
854 };
855
856 security_schemes.insert(name.clone(), scheme_info);
857 }
858
859 security_schemes
860}
861
862fn convert_openapi_security_scheme(
864 _name: &str,
865 scheme: &SecurityScheme,
866) -> Option<SecuritySchemeInfo> {
867 match scheme {
868 SecurityScheme::APIKey {
869 location,
870 name: param_name,
871 description,
872 ..
873 } => {
874 let location_str = match location {
875 openapiv3::APIKeyLocation::Query => constants::PARAM_LOCATION_QUERY,
876 openapiv3::APIKeyLocation::Header => constants::PARAM_LOCATION_HEADER,
877 openapiv3::APIKeyLocation::Cookie => constants::PARAM_LOCATION_COOKIE,
878 };
879
880 let aperture_secret = extract_aperture_secret_from_extensions(scheme);
881
882 Some(SecuritySchemeInfo {
883 scheme_type: constants::AUTH_SCHEME_APIKEY.to_string(),
884 description: description.clone(),
885 details: SecuritySchemeDetails::ApiKey {
886 location: location_str.to_string(),
887 name: param_name.clone(),
888 },
889 aperture_secret,
890 })
891 }
892 SecurityScheme::HTTP {
893 scheme: http_scheme,
894 bearer_format,
895 description,
896 ..
897 } => {
898 let details = match http_scheme.as_str() {
899 constants::AUTH_SCHEME_BEARER => SecuritySchemeDetails::HttpBearer {
900 bearer_format: bearer_format.clone(),
901 },
902 constants::AUTH_SCHEME_BASIC => SecuritySchemeDetails::HttpBasic,
903 _ => SecuritySchemeDetails::HttpBearer {
904 bearer_format: None,
905 },
906 };
907
908 let aperture_secret = extract_aperture_secret_from_extensions(scheme);
909
910 Some(SecuritySchemeInfo {
911 scheme_type: constants::SECURITY_TYPE_HTTP.to_string(),
912 description: description.clone(),
913 details,
914 aperture_secret,
915 })
916 }
917 SecurityScheme::OAuth2 { .. } | SecurityScheme::OpenIDConnect { .. } => None,
918 }
919}
920
921fn extract_aperture_secret_from_extensions(
923 scheme: &SecurityScheme,
924) -> Option<CachedApertureSecret> {
925 let extensions = match scheme {
926 SecurityScheme::APIKey { extensions, .. } | SecurityScheme::HTTP { extensions, .. } => {
927 extensions
928 }
929 SecurityScheme::OAuth2 { .. } | SecurityScheme::OpenIDConnect { .. } => return None,
930 };
931
932 extensions
933 .get(constants::EXT_APERTURE_SECRET)
934 .and_then(|value| {
935 let obj = value.as_object()?;
936 let source = obj.get(constants::EXT_KEY_SOURCE)?.as_str()?;
937 let name = obj.get(constants::EXT_KEY_NAME)?.as_str()?;
938
939 if source != constants::SOURCE_ENV {
940 return None;
941 }
942
943 Some(CachedApertureSecret {
944 source: source.to_string(),
945 name: name.to_string(),
946 })
947 })
948}
949
950#[cfg(test)]
951mod tests {
952 use super::*;
953 use crate::cache::models::{
954 CachedApertureSecret, CachedCommand, CachedParameter, CachedSecurityScheme, CachedSpec,
955 };
956
957 #[test]
958 fn test_command_name_conversion() {
959 assert_eq!(to_kebab_case("getUserById"), "get-user-by-id");
961 assert_eq!(to_kebab_case("createUser"), "create-user");
962 assert_eq!(to_kebab_case("list"), "list");
963 assert_eq!(to_kebab_case("GET"), "get");
964 assert_eq!(
965 to_kebab_case("List an Organization's Issues"),
966 "list-an-organizations-issues"
967 );
968 }
969
970 #[test]
971 fn test_generate_capability_manifest() {
972 let mut security_schemes = HashMap::new();
973 security_schemes.insert(
974 "bearerAuth".to_string(),
975 CachedSecurityScheme {
976 name: "bearerAuth".to_string(),
977 scheme_type: constants::SECURITY_TYPE_HTTP.to_string(),
978 scheme: Some(constants::AUTH_SCHEME_BEARER.to_string()),
979 location: Some(constants::LOCATION_HEADER.to_string()),
980 parameter_name: Some(constants::HEADER_AUTHORIZATION.to_string()),
981 description: None,
982 bearer_format: None,
983 aperture_secret: Some(CachedApertureSecret {
984 source: constants::SOURCE_ENV.to_string(),
985 name: "API_TOKEN".to_string(),
986 }),
987 },
988 );
989
990 let spec = CachedSpec {
991 cache_format_version: crate::cache::models::CACHE_FORMAT_VERSION,
992 name: "Test API".to_string(),
993 version: "1.0.0".to_string(),
994 commands: vec![CachedCommand {
995 name: "users".to_string(),
996 description: Some("Get user by ID".to_string()),
997 summary: None,
998 operation_id: "getUserById".to_string(),
999 method: constants::HTTP_METHOD_GET.to_string(),
1000 path: "/users/{id}".to_string(),
1001 parameters: vec![CachedParameter {
1002 name: "id".to_string(),
1003 location: constants::PARAM_LOCATION_PATH.to_string(),
1004 required: true,
1005 description: None,
1006 schema: Some(constants::SCHEMA_TYPE_STRING.to_string()),
1007 schema_type: Some(constants::SCHEMA_TYPE_STRING.to_string()),
1008 format: None,
1009 default_value: None,
1010 enum_values: vec![],
1011 example: None,
1012 }],
1013 request_body: None,
1014 responses: vec![],
1015 security_requirements: vec!["bearerAuth".to_string()],
1016 tags: vec!["users".to_string()],
1017 deprecated: false,
1018 external_docs_url: None,
1019 examples: vec![],
1020 }],
1021 base_url: Some("https://test-api.example.com".to_string()),
1022 servers: vec!["https://test-api.example.com".to_string()],
1023 security_schemes,
1024 skipped_endpoints: vec![],
1025 server_variables: HashMap::new(),
1026 };
1027
1028 let manifest_json = generate_capability_manifest(&spec, None).unwrap();
1029 let manifest: ApiCapabilityManifest = serde_json::from_str(&manifest_json).unwrap();
1030
1031 assert_eq!(manifest.api.name, "Test API");
1032 assert_eq!(manifest.api.version, "1.0.0");
1033 assert!(manifest.commands.contains_key("users"));
1034
1035 let users_commands = &manifest.commands["users"];
1036 assert_eq!(users_commands.len(), 1);
1037 assert_eq!(users_commands[0].name, "get-user-by-id");
1038 assert_eq!(users_commands[0].method, constants::HTTP_METHOD_GET);
1039 assert_eq!(users_commands[0].parameters.len(), 1);
1040 assert_eq!(users_commands[0].parameters[0].name, "id");
1041
1042 assert!(!manifest.security_schemes.is_empty());
1044 assert!(manifest.security_schemes.contains_key("bearerAuth"));
1045 let bearer_auth = &manifest.security_schemes["bearerAuth"];
1046 assert_eq!(bearer_auth.scheme_type, constants::SECURITY_TYPE_HTTP);
1047 assert!(matches!(
1048 &bearer_auth.details,
1049 SecuritySchemeDetails::HttpBearer { .. }
1050 ));
1051 assert!(bearer_auth.aperture_secret.is_some());
1052 let aperture_secret = bearer_auth.aperture_secret.as_ref().unwrap();
1053 assert_eq!(aperture_secret.name, "API_TOKEN");
1054 assert_eq!(aperture_secret.source, constants::SOURCE_ENV);
1055 }
1056}