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 #[serde(skip_serializing_if = "Option::is_none")]
100 pub display_group: Option<String>,
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub display_name: Option<String>,
104 #[serde(skip_serializing_if = "Vec::is_empty", default)]
106 pub aliases: Vec<String>,
107 #[serde(skip_serializing_if = "std::ops::Not::not", default)]
109 pub hidden: bool,
110}
111
112#[derive(Debug, Serialize, Deserialize)]
113pub struct ParameterInfo {
114 pub name: String,
116 pub location: String,
118 pub required: bool,
120 pub param_type: String,
122 pub description: Option<String>,
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub format: Option<String>,
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub default_value: Option<String>,
130 #[serde(skip_serializing_if = "Vec::is_empty", default)]
132 pub enum_values: Vec<String>,
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub example: Option<String>,
136}
137
138#[derive(Debug, Serialize, Deserialize)]
139pub struct RequestBodyInfo {
140 pub required: bool,
142 pub content_type: String,
144 pub description: Option<String>,
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub example: Option<String>,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct ResponseSchemaInfo {
175 pub content_type: String,
177 pub schema: serde_json::Value,
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub example: Option<serde_json::Value>,
185}
186
187#[derive(Debug, Serialize, Deserialize)]
189pub struct SecuritySchemeInfo {
190 #[serde(rename = "type")]
192 pub scheme_type: String,
193 pub description: Option<String>,
195 #[serde(flatten)]
197 pub details: SecuritySchemeDetails,
198 #[serde(rename = "x-aperture-secret", skip_serializing_if = "Option::is_none")]
200 pub aperture_secret: Option<CachedApertureSecret>,
201}
202
203#[derive(Debug, Serialize, Deserialize)]
205#[serde(tag = "scheme", rename_all = "camelCase")]
206pub enum SecuritySchemeDetails {
207 #[serde(rename = "bearer")]
209 HttpBearer {
210 #[serde(skip_serializing_if = "Option::is_none")]
212 bearer_format: Option<String>,
213 },
214 #[serde(rename = "basic")]
216 HttpBasic,
217 #[serde(rename = "apiKey")]
219 ApiKey {
220 #[serde(rename = "in")]
222 location: String,
223 name: String,
225 },
226}
227
228pub fn generate_capability_manifest_from_openapi(
246 api_name: &str,
247 spec: &OpenAPI,
248 cached_spec: &CachedSpec,
249 global_config: Option<&GlobalConfig>,
250) -> Result<String, Error> {
251 let base_url = spec.servers.first().map(|s| s.url.clone());
253 let servers: Vec<String> = spec.servers.iter().map(|s| s.url.clone()).collect();
254
255 let temp_cached_spec = CachedSpec {
256 cache_format_version: crate::cache::models::CACHE_FORMAT_VERSION,
257 name: api_name.to_string(),
258 version: spec.info.version.clone(),
259 commands: vec![], base_url,
261 servers,
262 security_schemes: HashMap::new(), skipped_endpoints: vec![], server_variables: HashMap::new(), };
266
267 let resolver = BaseUrlResolver::new(&temp_cached_spec);
269 let resolver = if let Some(config) = global_config {
270 resolver.with_global_config(config)
271 } else {
272 resolver
273 };
274 let resolved_base_url = resolver.resolve(None);
275
276 let mut command_groups: HashMap<String, Vec<CommandInfo>> = HashMap::new();
278
279 let skipped_set: std::collections::HashSet<(&str, &str)> = cached_spec
281 .skipped_endpoints
282 .iter()
283 .map(|ep| (ep.path.as_str(), ep.method.as_str()))
284 .collect();
285
286 for (path, path_item) in &spec.paths.paths {
287 let ReferenceOr::Item(item) = path_item else {
288 continue;
289 };
290
291 for (method, operation) in crate::spec::http_methods_iter(item) {
293 let Some(op) = operation else {
294 continue;
295 };
296
297 if skipped_set.contains(&(path.as_str(), method.to_uppercase().as_str())) {
299 continue;
300 }
301
302 let command_info =
303 convert_openapi_operation_to_info(method, path, op, spec, spec.security.as_ref());
304
305 let group_name = op.tags.first().map_or_else(
307 || constants::DEFAULT_GROUP.to_string(),
308 |tag| to_kebab_case(tag),
309 );
310
311 command_groups
312 .entry(group_name)
313 .or_default()
314 .push(command_info);
315 }
316 }
317
318 let mapping_index: HashMap<&str, &CachedCommand> = cached_spec
325 .commands
326 .iter()
327 .map(|c| (c.operation_id.as_str(), c))
328 .collect();
329
330 let mut regrouped: HashMap<String, Vec<CommandInfo>> = HashMap::new();
333 for (_group, commands) in command_groups {
334 for mut cmd_info in commands {
335 if let Some(cached_cmd) = mapping_index.get(cmd_info.operation_id.as_str()) {
336 cmd_info.display_group.clone_from(&cached_cmd.display_group);
337 cmd_info.display_name.clone_from(&cached_cmd.display_name);
338 cmd_info.aliases.clone_from(&cached_cmd.aliases);
339 cmd_info.hidden = cached_cmd.hidden;
340 }
341
342 let effective_group = cmd_info.display_group.as_ref().map_or_else(
344 || {
345 cmd_info.original_tags.first().map_or_else(
346 || constants::DEFAULT_GROUP.to_string(),
347 |tag| to_kebab_case(tag),
348 )
349 },
350 |g| to_kebab_case(g),
351 );
352
353 regrouped.entry(effective_group).or_default().push(cmd_info);
354 }
355 }
356
357 let security_schemes = extract_security_schemes_from_openapi(spec);
359
360 let skipped = cached_spec.skipped_endpoints.len();
362 let available = cached_spec.commands.len();
363 let total = available + skipped;
364
365 let manifest = ApiCapabilityManifest {
367 api: ApiInfo {
368 name: spec.info.title.clone(),
369 version: spec.info.version.clone(),
370 description: spec.info.description.clone(),
371 base_url: resolved_base_url,
372 },
373 endpoints: EndpointStatistics {
374 total,
375 available,
376 skipped,
377 },
378 commands: regrouped,
379 security_schemes,
380 };
381
382 serde_json::to_string_pretty(&manifest)
384 .map_err(|e| Error::serialization_error(format!("Failed to serialize agent manifest: {e}")))
385}
386
387pub fn generate_capability_manifest(
403 spec: &CachedSpec,
404 global_config: Option<&GlobalConfig>,
405) -> Result<String, Error> {
406 let mut command_groups: HashMap<String, Vec<CommandInfo>> = HashMap::new();
407
408 for cached_command in &spec.commands {
410 let group_name = if cached_command.name.is_empty() {
411 constants::DEFAULT_GROUP.to_string()
412 } else {
413 to_kebab_case(&cached_command.name)
414 };
415
416 let command_info = convert_cached_command_to_info(cached_command);
417 command_groups
418 .entry(group_name)
419 .or_default()
420 .push(command_info);
421 }
422
423 let resolver = BaseUrlResolver::new(spec);
425 let resolver = if let Some(config) = global_config {
426 resolver.with_global_config(config)
427 } else {
428 resolver
429 };
430 let base_url = resolver.resolve(None);
431
432 let skipped = spec.skipped_endpoints.len();
434 let available = spec.commands.len();
435 let total = available + skipped;
436
437 let manifest = ApiCapabilityManifest {
439 api: ApiInfo {
440 name: spec.name.clone(),
441 version: spec.version.clone(),
442 description: None, base_url,
444 },
445 endpoints: EndpointStatistics {
446 total,
447 available,
448 skipped,
449 },
450 commands: command_groups,
451 security_schemes: extract_security_schemes(spec),
452 };
453
454 serde_json::to_string_pretty(&manifest)
456 .map_err(|e| Error::serialization_error(format!("Failed to serialize agent manifest: {e}")))
457}
458
459fn convert_cached_command_to_info(cached_command: &CachedCommand) -> CommandInfo {
461 let command_name = if cached_command.operation_id.is_empty() {
462 cached_command.method.to_lowercase()
463 } else {
464 to_kebab_case(&cached_command.operation_id)
465 };
466
467 let parameters: Vec<ParameterInfo> = cached_command
468 .parameters
469 .iter()
470 .map(convert_cached_parameter_to_info)
471 .collect();
472
473 let request_body = cached_command
474 .request_body
475 .as_ref()
476 .map(convert_cached_request_body_to_info);
477
478 let response_schema = extract_response_schema_from_cached(&cached_command.responses);
480
481 CommandInfo {
482 name: command_name,
483 method: cached_command.method.clone(),
484 path: cached_command.path.clone(),
485 description: cached_command.description.clone(),
486 summary: cached_command.summary.clone(),
487 operation_id: cached_command.operation_id.clone(),
488 parameters,
489 request_body,
490 security_requirements: cached_command.security_requirements.clone(),
491 tags: cached_command
492 .tags
493 .iter()
494 .map(|t| to_kebab_case(t))
495 .collect(),
496 original_tags: cached_command.tags.clone(),
497 deprecated: cached_command.deprecated,
498 external_docs_url: cached_command.external_docs_url.clone(),
499 response_schema,
500 display_group: cached_command.display_group.clone(),
501 display_name: cached_command.display_name.clone(),
502 aliases: cached_command.aliases.clone(),
503 hidden: cached_command.hidden,
504 }
505}
506
507fn convert_cached_parameter_to_info(cached_param: &CachedParameter) -> ParameterInfo {
509 ParameterInfo {
510 name: cached_param.name.clone(),
511 location: cached_param.location.clone(),
512 required: cached_param.required,
513 param_type: cached_param
514 .schema_type
515 .clone()
516 .unwrap_or_else(|| constants::SCHEMA_TYPE_STRING.to_string()),
517 description: cached_param.description.clone(),
518 format: cached_param.format.clone(),
519 default_value: cached_param.default_value.clone(),
520 enum_values: cached_param.enum_values.clone(),
521 example: cached_param.example.clone(),
522 }
523}
524
525fn convert_cached_request_body_to_info(cached_body: &CachedRequestBody) -> RequestBodyInfo {
527 RequestBodyInfo {
528 required: cached_body.required,
529 content_type: cached_body.content_type.clone(),
530 description: cached_body.description.clone(),
531 example: cached_body.example.clone(),
532 }
533}
534
535fn extract_response_schema_from_cached(
541 responses: &[crate::cache::models::CachedResponse],
542) -> Option<ResponseSchemaInfo> {
543 constants::SUCCESS_STATUS_CODES.iter().find_map(|code| {
544 responses
545 .iter()
546 .find(|r| r.status_code == *code)
547 .and_then(|response| {
548 let content_type = response.content_type.as_ref()?;
549 let schema_str = response.schema.as_ref()?;
550 let schema = serde_json::from_str(schema_str).ok()?;
551 let example = response
552 .example
553 .as_ref()
554 .and_then(|ex| serde_json::from_str(ex).ok());
555
556 Some(ResponseSchemaInfo {
557 content_type: content_type.clone(),
558 schema,
559 example,
560 })
561 })
562 })
563}
564
565fn extract_security_schemes(spec: &CachedSpec) -> HashMap<String, SecuritySchemeInfo> {
567 let mut security_schemes = HashMap::new();
568
569 for (name, scheme) in &spec.security_schemes {
570 let details = match scheme.scheme_type.as_str() {
571 constants::SECURITY_TYPE_HTTP => {
572 scheme.scheme.as_ref().map_or(
573 SecuritySchemeDetails::HttpBearer {
574 bearer_format: None,
575 },
576 |http_scheme| match http_scheme.as_str() {
577 constants::AUTH_SCHEME_BEARER => SecuritySchemeDetails::HttpBearer {
578 bearer_format: scheme.bearer_format.clone(),
579 },
580 constants::AUTH_SCHEME_BASIC => SecuritySchemeDetails::HttpBasic,
581 _ => {
582 SecuritySchemeDetails::HttpBearer {
584 bearer_format: None,
585 }
586 }
587 },
588 )
589 }
590 constants::AUTH_SCHEME_APIKEY => SecuritySchemeDetails::ApiKey {
591 location: scheme
592 .location
593 .clone()
594 .unwrap_or_else(|| constants::LOCATION_HEADER.to_string()),
595 name: scheme
596 .parameter_name
597 .clone()
598 .unwrap_or_else(|| constants::HEADER_AUTHORIZATION.to_string()),
599 },
600 _ => {
601 SecuritySchemeDetails::HttpBearer {
603 bearer_format: None,
604 }
605 }
606 };
607
608 let scheme_info = SecuritySchemeInfo {
609 scheme_type: scheme.scheme_type.clone(),
610 description: scheme.description.clone(),
611 details,
612 aperture_secret: scheme.aperture_secret.clone(),
613 };
614
615 security_schemes.insert(name.clone(), scheme_info);
616 }
617
618 security_schemes
619}
620
621fn convert_openapi_operation_to_info(
623 method: &str,
624 path: &str,
625 operation: &Operation,
626 spec: &OpenAPI,
627 global_security: Option<&Vec<openapiv3::SecurityRequirement>>,
628) -> CommandInfo {
629 let command_name = operation
630 .operation_id
631 .as_ref()
632 .map_or_else(|| method.to_lowercase(), |op_id| to_kebab_case(op_id));
633
634 let parameters: Vec<ParameterInfo> = operation
636 .parameters
637 .iter()
638 .filter_map(|param_ref| match param_ref {
639 ReferenceOr::Item(param) => Some(convert_openapi_parameter_to_info(param)),
640 ReferenceOr::Reference { reference } => resolve_parameter_reference(spec, reference)
641 .ok()
642 .map(|param| convert_openapi_parameter_to_info(¶m)),
643 })
644 .collect();
645
646 let request_body = operation.request_body.as_ref().and_then(|rb_ref| {
648 let ReferenceOr::Item(body) = rb_ref else {
649 return None;
650 };
651
652 let content_type = if body.content.contains_key(constants::CONTENT_TYPE_JSON) {
654 constants::CONTENT_TYPE_JSON
655 } else {
656 body.content.keys().next().map(String::as_str)?
657 };
658
659 let media_type = body.content.get(content_type)?;
660 let example = media_type
661 .example
662 .as_ref()
663 .map(|ex| serde_json::to_string(ex).unwrap_or_else(|_| ex.to_string()));
664
665 Some(RequestBodyInfo {
666 required: body.required,
667 content_type: content_type.to_string(),
668 description: body.description.clone(),
669 example,
670 })
671 });
672
673 let security_requirements = operation.security.as_ref().map_or_else(
675 || {
676 global_security.map_or(vec![], |reqs| {
677 reqs.iter().flat_map(|req| req.keys().cloned()).collect()
678 })
679 },
680 |op_security| {
681 op_security
682 .iter()
683 .flat_map(|req| req.keys().cloned())
684 .collect()
685 },
686 );
687
688 let response_schema = extract_response_schema_from_operation(operation, spec);
690
691 CommandInfo {
692 name: command_name,
693 method: method.to_uppercase(),
694 path: path.to_string(),
695 description: operation.description.clone(),
696 summary: operation.summary.clone(),
697 operation_id: operation.operation_id.clone().unwrap_or_default(),
698 parameters,
699 request_body,
700 security_requirements,
701 tags: operation.tags.iter().map(|t| to_kebab_case(t)).collect(),
702 original_tags: operation.tags.clone(),
703 deprecated: operation.deprecated,
704 external_docs_url: operation
705 .external_docs
706 .as_ref()
707 .map(|docs| docs.url.clone()),
708 response_schema,
709 display_group: None,
712 display_name: None,
713 aliases: vec![],
714 hidden: false,
715 }
716}
717
718fn extract_response_schema_from_operation(
723 operation: &Operation,
724 spec: &OpenAPI,
725) -> Option<ResponseSchemaInfo> {
726 constants::SUCCESS_STATUS_CODES.iter().find_map(|code| {
727 operation
728 .responses
729 .responses
730 .get(&openapiv3::StatusCode::Code(
731 code.parse().expect("valid status code"),
732 ))
733 .and_then(|response_ref| extract_response_schema_from_response(response_ref, spec))
734 })
735}
736
737fn extract_response_schema_from_response(
750 response_ref: &ReferenceOr<openapiv3::Response>,
751 spec: &OpenAPI,
752) -> Option<ResponseSchemaInfo> {
753 let ReferenceOr::Item(response) = response_ref else {
757 return None;
758 };
759
760 let content_type = if response.content.contains_key(constants::CONTENT_TYPE_JSON) {
762 constants::CONTENT_TYPE_JSON
763 } else {
764 response.content.keys().next().map(String::as_str)?
766 };
767
768 let media_type = response.content.get(content_type)?;
769 let schema_ref = media_type.schema.as_ref()?;
770
771 let schema_value = match schema_ref {
773 ReferenceOr::Item(schema) => serde_json::to_value(schema).ok()?,
774 ReferenceOr::Reference { reference } => {
775 let resolved = resolve_schema_reference(spec, reference).ok()?;
776 serde_json::to_value(&resolved).ok()?
777 }
778 };
779
780 let example = media_type
782 .example
783 .as_ref()
784 .and_then(|ex| serde_json::to_value(ex).ok());
785
786 Some(ResponseSchemaInfo {
787 content_type: content_type.to_string(),
788 schema: schema_value,
789 example,
790 })
791}
792
793fn extract_schema_info_from_parameter(
795 format: &openapiv3::ParameterSchemaOrContent,
796) -> ParameterSchemaInfo {
797 let openapiv3::ParameterSchemaOrContent::Schema(schema_ref) = format else {
798 return (
799 Some(constants::SCHEMA_TYPE_STRING.to_string()),
800 None,
801 None,
802 vec![],
803 None,
804 );
805 };
806
807 match schema_ref {
808 ReferenceOr::Item(schema) => {
809 let (schema_type, format, enums) =
810 extract_schema_type_from_schema_kind(&schema.schema_kind);
811
812 let default_value = schema
813 .schema_data
814 .default
815 .as_ref()
816 .map(|v| serde_json::to_string(v).unwrap_or_else(|_| v.to_string()));
817
818 (Some(schema_type), format, default_value, enums, None)
819 }
820 ReferenceOr::Reference { .. } => (
821 Some(constants::SCHEMA_TYPE_STRING.to_string()),
822 None,
823 None,
824 vec![],
825 None,
826 ),
827 }
828}
829
830fn extract_schema_type_from_schema_kind(
832 schema_kind: &openapiv3::SchemaKind,
833) -> (String, Option<String>, Vec<String>) {
834 match schema_kind {
835 openapiv3::SchemaKind::Type(type_val) => match type_val {
836 openapiv3::Type::String(string_type) => {
837 let enum_values: Vec<String> = string_type
838 .enumeration
839 .iter()
840 .filter_map(|v| v.as_ref())
841 .map(|v| serde_json::to_string(v).unwrap_or_else(|_| v.clone()))
842 .collect();
843 (constants::SCHEMA_TYPE_STRING.to_string(), None, enum_values)
844 }
845 openapiv3::Type::Number(_) => (constants::SCHEMA_TYPE_NUMBER.to_string(), None, vec![]),
846 openapiv3::Type::Integer(_) => {
847 (constants::SCHEMA_TYPE_INTEGER.to_string(), None, vec![])
848 }
849 openapiv3::Type::Boolean(_) => {
850 (constants::SCHEMA_TYPE_BOOLEAN.to_string(), None, vec![])
851 }
852 openapiv3::Type::Array(_) => (constants::SCHEMA_TYPE_ARRAY.to_string(), None, vec![]),
853 openapiv3::Type::Object(_) => (constants::SCHEMA_TYPE_OBJECT.to_string(), None, vec![]),
854 },
855 _ => (constants::SCHEMA_TYPE_STRING.to_string(), None, vec![]),
856 }
857}
858
859fn convert_openapi_parameter_to_info(param: &OpenApiParameter) -> ParameterInfo {
861 let (param_data, location_str) = match param {
862 OpenApiParameter::Query { parameter_data, .. } => {
863 (parameter_data, constants::PARAM_LOCATION_QUERY)
864 }
865 OpenApiParameter::Header { parameter_data, .. } => {
866 (parameter_data, constants::PARAM_LOCATION_HEADER)
867 }
868 OpenApiParameter::Path { parameter_data, .. } => {
869 (parameter_data, constants::PARAM_LOCATION_PATH)
870 }
871 OpenApiParameter::Cookie { parameter_data, .. } => {
872 (parameter_data, constants::PARAM_LOCATION_COOKIE)
873 }
874 };
875
876 let (schema_type, format, default_value, enum_values, example) =
878 extract_schema_info_from_parameter(¶m_data.format);
879
880 let example = param_data
882 .example
883 .as_ref()
884 .map(|ex| serde_json::to_string(ex).unwrap_or_else(|_| ex.to_string()))
885 .or(example);
886
887 ParameterInfo {
888 name: param_data.name.clone(),
889 location: location_str.to_string(),
890 required: param_data.required,
891 param_type: schema_type.unwrap_or_else(|| constants::SCHEMA_TYPE_STRING.to_string()),
892 description: param_data.description.clone(),
893 format,
894 default_value,
895 enum_values,
896 example,
897 }
898}
899
900fn extract_security_schemes_from_openapi(spec: &OpenAPI) -> HashMap<String, SecuritySchemeInfo> {
902 let mut security_schemes = HashMap::new();
903
904 let Some(components) = &spec.components else {
905 return security_schemes;
906 };
907
908 for (name, scheme_ref) in &components.security_schemes {
909 let ReferenceOr::Item(scheme) = scheme_ref else {
910 continue;
911 };
912
913 let Some(scheme_info) = convert_openapi_security_scheme(name, scheme) else {
914 continue;
915 };
916
917 security_schemes.insert(name.clone(), scheme_info);
918 }
919
920 security_schemes
921}
922
923fn convert_openapi_security_scheme(
925 _name: &str,
926 scheme: &SecurityScheme,
927) -> Option<SecuritySchemeInfo> {
928 match scheme {
929 SecurityScheme::APIKey {
930 location,
931 name: param_name,
932 description,
933 ..
934 } => {
935 let location_str = match location {
936 openapiv3::APIKeyLocation::Query => constants::PARAM_LOCATION_QUERY,
937 openapiv3::APIKeyLocation::Header => constants::PARAM_LOCATION_HEADER,
938 openapiv3::APIKeyLocation::Cookie => constants::PARAM_LOCATION_COOKIE,
939 };
940
941 let aperture_secret = extract_aperture_secret_from_extensions(scheme);
942
943 Some(SecuritySchemeInfo {
944 scheme_type: constants::AUTH_SCHEME_APIKEY.to_string(),
945 description: description.clone(),
946 details: SecuritySchemeDetails::ApiKey {
947 location: location_str.to_string(),
948 name: param_name.clone(),
949 },
950 aperture_secret,
951 })
952 }
953 SecurityScheme::HTTP {
954 scheme: http_scheme,
955 bearer_format,
956 description,
957 ..
958 } => {
959 let details = match http_scheme.as_str() {
960 constants::AUTH_SCHEME_BEARER => SecuritySchemeDetails::HttpBearer {
961 bearer_format: bearer_format.clone(),
962 },
963 constants::AUTH_SCHEME_BASIC => SecuritySchemeDetails::HttpBasic,
964 _ => SecuritySchemeDetails::HttpBearer {
965 bearer_format: None,
966 },
967 };
968
969 let aperture_secret = extract_aperture_secret_from_extensions(scheme);
970
971 Some(SecuritySchemeInfo {
972 scheme_type: constants::SECURITY_TYPE_HTTP.to_string(),
973 description: description.clone(),
974 details,
975 aperture_secret,
976 })
977 }
978 SecurityScheme::OAuth2 { .. } | SecurityScheme::OpenIDConnect { .. } => None,
979 }
980}
981
982fn extract_aperture_secret_from_extensions(
984 scheme: &SecurityScheme,
985) -> Option<CachedApertureSecret> {
986 let extensions = match scheme {
987 SecurityScheme::APIKey { extensions, .. } | SecurityScheme::HTTP { extensions, .. } => {
988 extensions
989 }
990 SecurityScheme::OAuth2 { .. } | SecurityScheme::OpenIDConnect { .. } => return None,
991 };
992
993 extensions
994 .get(constants::EXT_APERTURE_SECRET)
995 .and_then(|value| {
996 let obj = value.as_object()?;
997 let source = obj.get(constants::EXT_KEY_SOURCE)?.as_str()?;
998 let name = obj.get(constants::EXT_KEY_NAME)?.as_str()?;
999
1000 if source != constants::SOURCE_ENV {
1001 return None;
1002 }
1003
1004 Some(CachedApertureSecret {
1005 source: source.to_string(),
1006 name: name.to_string(),
1007 })
1008 })
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use super::*;
1014 use crate::cache::models::{
1015 CachedApertureSecret, CachedCommand, CachedParameter, CachedSecurityScheme, CachedSpec,
1016 };
1017
1018 #[test]
1019 fn test_command_name_conversion() {
1020 assert_eq!(to_kebab_case("getUserById"), "get-user-by-id");
1022 assert_eq!(to_kebab_case("createUser"), "create-user");
1023 assert_eq!(to_kebab_case("list"), "list");
1024 assert_eq!(to_kebab_case("GET"), "get");
1025 assert_eq!(
1026 to_kebab_case("List an Organization's Issues"),
1027 "list-an-organizations-issues"
1028 );
1029 }
1030
1031 #[test]
1032 fn test_generate_capability_manifest() {
1033 let mut security_schemes = HashMap::new();
1034 security_schemes.insert(
1035 "bearerAuth".to_string(),
1036 CachedSecurityScheme {
1037 name: "bearerAuth".to_string(),
1038 scheme_type: constants::SECURITY_TYPE_HTTP.to_string(),
1039 scheme: Some(constants::AUTH_SCHEME_BEARER.to_string()),
1040 location: Some(constants::LOCATION_HEADER.to_string()),
1041 parameter_name: Some(constants::HEADER_AUTHORIZATION.to_string()),
1042 description: None,
1043 bearer_format: None,
1044 aperture_secret: Some(CachedApertureSecret {
1045 source: constants::SOURCE_ENV.to_string(),
1046 name: "API_TOKEN".to_string(),
1047 }),
1048 },
1049 );
1050
1051 let spec = CachedSpec {
1052 cache_format_version: crate::cache::models::CACHE_FORMAT_VERSION,
1053 name: "Test API".to_string(),
1054 version: "1.0.0".to_string(),
1055 commands: vec![CachedCommand {
1056 name: "users".to_string(),
1057 description: Some("Get user by ID".to_string()),
1058 summary: None,
1059 operation_id: "getUserById".to_string(),
1060 method: constants::HTTP_METHOD_GET.to_string(),
1061 path: "/users/{id}".to_string(),
1062 parameters: vec![CachedParameter {
1063 name: "id".to_string(),
1064 location: constants::PARAM_LOCATION_PATH.to_string(),
1065 required: true,
1066 description: None,
1067 schema: Some(constants::SCHEMA_TYPE_STRING.to_string()),
1068 schema_type: Some(constants::SCHEMA_TYPE_STRING.to_string()),
1069 format: None,
1070 default_value: None,
1071 enum_values: vec![],
1072 example: None,
1073 }],
1074 request_body: None,
1075 responses: vec![],
1076 security_requirements: vec!["bearerAuth".to_string()],
1077 tags: vec!["users".to_string()],
1078 deprecated: false,
1079 external_docs_url: None,
1080 examples: vec![],
1081 display_group: None,
1082 display_name: None,
1083 aliases: vec![],
1084 hidden: false,
1085 }],
1086 base_url: Some("https://test-api.example.com".to_string()),
1087 servers: vec!["https://test-api.example.com".to_string()],
1088 security_schemes,
1089 skipped_endpoints: vec![],
1090 server_variables: HashMap::new(),
1091 };
1092
1093 let manifest_json = generate_capability_manifest(&spec, None).unwrap();
1094 let manifest: ApiCapabilityManifest = serde_json::from_str(&manifest_json).unwrap();
1095
1096 assert_eq!(manifest.api.name, "Test API");
1097 assert_eq!(manifest.api.version, "1.0.0");
1098 assert!(manifest.commands.contains_key("users"));
1099
1100 let users_commands = &manifest.commands["users"];
1101 assert_eq!(users_commands.len(), 1);
1102 assert_eq!(users_commands[0].name, "get-user-by-id");
1103 assert_eq!(users_commands[0].method, constants::HTTP_METHOD_GET);
1104 assert_eq!(users_commands[0].parameters.len(), 1);
1105 assert_eq!(users_commands[0].parameters[0].name, "id");
1106
1107 assert!(!manifest.security_schemes.is_empty());
1109 assert!(manifest.security_schemes.contains_key("bearerAuth"));
1110 let bearer_auth = &manifest.security_schemes["bearerAuth"];
1111 assert_eq!(bearer_auth.scheme_type, constants::SECURITY_TYPE_HTTP);
1112 assert!(matches!(
1113 &bearer_auth.details,
1114 SecuritySchemeDetails::HttpBearer { .. }
1115 ));
1116 assert!(bearer_auth.aperture_secret.is_some());
1117 let aperture_secret = bearer_auth.aperture_secret.as_ref().unwrap();
1118 assert_eq!(aperture_secret.name, "API_TOKEN");
1119 assert_eq!(aperture_secret.source, constants::SOURCE_ENV);
1120 }
1121}