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