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