1use crate::cache::models::{
2 CachedApertureSecret, CachedCommand, CachedParameter, CachedRequestBody, CachedResponse,
3 CachedSecurityScheme, CachedSpec, SkippedEndpoint, CACHE_FORMAT_VERSION,
4};
5use crate::error::Error;
6use openapiv3::{OpenAPI, Operation, Parameter, ReferenceOr, RequestBody, SecurityScheme};
7use serde_json;
8use std::collections::HashMap;
9
10#[derive(Debug, Clone)]
12pub struct TransformOptions {
13 pub name: String,
15 pub skip_endpoints: Vec<(String, String)>,
17 pub warnings: Vec<crate::spec::validator::ValidationWarning>,
19}
20
21impl TransformOptions {
22 #[must_use]
24 pub fn new(name: impl Into<String>) -> Self {
25 Self {
26 name: name.into(),
27 skip_endpoints: Vec::new(),
28 warnings: Vec::new(),
29 }
30 }
31
32 #[must_use]
34 pub fn with_skip_endpoints(mut self, endpoints: Vec<(String, String)>) -> Self {
35 self.skip_endpoints = endpoints;
36 self
37 }
38
39 #[must_use]
41 pub fn with_warnings(
42 mut self,
43 warnings: Vec<crate::spec::validator::ValidationWarning>,
44 ) -> Self {
45 self.warnings = warnings;
46 self
47 }
48}
49
50pub struct SpecTransformer;
52
53impl SpecTransformer {
54 #[must_use]
56 pub const fn new() -> Self {
57 Self
58 }
59
60 pub fn transform_with_options(
69 &self,
70 spec: &OpenAPI,
71 options: &TransformOptions,
72 ) -> Result<CachedSpec, Error> {
73 self.transform_with_warnings(
74 &options.name,
75 spec,
76 &options.skip_endpoints,
77 &options.warnings,
78 )
79 }
80
81 pub fn transform(&self, name: &str, spec: &OpenAPI) -> Result<CachedSpec, Error> {
90 self.transform_with_filter(name, spec, &[])
91 }
92
93 pub fn transform_with_filter(
108 &self,
109 name: &str,
110 spec: &OpenAPI,
111 skip_endpoints: &[(String, String)],
112 ) -> Result<CachedSpec, Error> {
113 self.transform_with_warnings(name, spec, skip_endpoints, &[])
114 }
115
116 pub fn transform_with_warnings(
129 &self,
130 name: &str,
131 spec: &OpenAPI,
132 skip_endpoints: &[(String, String)],
133 warnings: &[crate::spec::validator::ValidationWarning],
134 ) -> Result<CachedSpec, Error> {
135 let mut commands = Vec::new();
136
137 let version = spec.info.version.clone();
139
140 let servers: Vec<String> = spec.servers.iter().map(|s| s.url.clone()).collect();
142 let base_url = servers.first().cloned();
143
144 let server_variables: HashMap<String, crate::cache::models::ServerVariable> = spec
146 .servers
147 .first()
148 .and_then(|server| server.variables.as_ref())
149 .map(|vars| {
150 vars.iter()
151 .map(|(name, variable)| {
152 (
153 name.clone(),
154 crate::cache::models::ServerVariable {
155 default: Some(variable.default.clone()),
156 enum_values: variable.enumeration.clone(),
157 description: variable.description.clone(),
158 },
159 )
160 })
161 .collect()
162 })
163 .unwrap_or_default();
164
165 let global_security_requirements: Vec<String> = spec
167 .security
168 .iter()
169 .flat_map(|security_vec| {
170 security_vec
171 .iter()
172 .flat_map(|security_req| security_req.keys().cloned())
173 })
174 .collect();
175
176 for (path, path_item) in spec.paths.iter() {
178 Self::process_path_item(
179 spec,
180 path,
181 path_item,
182 skip_endpoints,
183 &global_security_requirements,
184 &mut commands,
185 )?;
186 }
187
188 let security_schemes = Self::extract_security_schemes(spec);
190
191 let skipped_endpoints: Vec<SkippedEndpoint> = warnings
193 .iter()
194 .map(|w| SkippedEndpoint {
195 path: w.endpoint.path.clone(),
196 method: w.endpoint.method.clone(),
197 content_type: w.endpoint.content_type.clone(),
198 reason: w.reason.clone(),
199 })
200 .collect();
201
202 Ok(CachedSpec {
203 cache_format_version: CACHE_FORMAT_VERSION,
204 name: name.to_string(),
205 version,
206 commands,
207 base_url,
208 servers,
209 security_schemes,
210 skipped_endpoints,
211 server_variables,
212 })
213 }
214
215 fn process_path_item(
217 spec: &OpenAPI,
218 path: &str,
219 path_item: &ReferenceOr<openapiv3::PathItem>,
220 skip_endpoints: &[(String, String)],
221 global_security_requirements: &[String],
222 commands: &mut Vec<CachedCommand>,
223 ) -> Result<(), Error> {
224 let ReferenceOr::Item(item) = path_item else {
225 return Ok(());
226 };
227
228 for (method, operation) in crate::spec::http_methods_iter(item) {
230 let Some(op) = operation else {
231 continue;
232 };
233
234 if Self::should_skip_endpoint(path, method, skip_endpoints) {
235 continue;
236 }
237
238 let command =
239 Self::transform_operation(spec, method, path, op, global_security_requirements)?;
240 commands.push(command);
241 }
242
243 Ok(())
244 }
245
246 fn should_skip_endpoint(path: &str, method: &str, skip_endpoints: &[(String, String)]) -> bool {
248 skip_endpoints.iter().any(|(skip_path, skip_method)| {
249 skip_path == path && skip_method.eq_ignore_ascii_case(method)
250 })
251 }
252
253 fn transform_operation(
255 spec: &OpenAPI,
256 method: &str,
257 path: &str,
258 operation: &Operation,
259 global_security_requirements: &[String],
260 ) -> Result<CachedCommand, Error> {
261 let operation_id = operation
263 .operation_id
264 .clone()
265 .unwrap_or_else(|| format!("{method}_{path}"));
266
267 let name = operation
269 .tags
270 .first()
271 .cloned()
272 .unwrap_or_else(|| "default".to_string());
273
274 let mut parameters = Vec::new();
276 for param_ref in &operation.parameters {
277 match param_ref {
278 ReferenceOr::Item(param) => {
279 parameters.push(Self::transform_parameter(param));
280 }
281 ReferenceOr::Reference { reference } => {
282 let param = Self::resolve_parameter_reference(spec, reference)?;
283 parameters.push(Self::transform_parameter(¶m));
284 }
285 }
286 }
287
288 let request_body = operation
290 .request_body
291 .as_ref()
292 .and_then(Self::transform_request_body);
293
294 let responses = operation
296 .responses
297 .responses
298 .iter()
299 .map(|(code, response_ref)| {
300 match response_ref {
301 ReferenceOr::Item(response) => {
302 let description = if response.description.is_empty() {
304 None
305 } else {
306 Some(response.description.clone())
307 };
308
309 let (content_type, schema) =
311 if let Some((ct, media_type)) = response.content.iter().next() {
312 let schema = media_type.schema.as_ref().and_then(|schema_ref| {
313 match schema_ref {
314 ReferenceOr::Item(schema) => {
315 serde_json::to_string(schema).ok()
316 }
317 ReferenceOr::Reference { .. } => None,
318 }
319 });
320 (Some(ct.clone()), schema)
321 } else {
322 (None, None)
323 };
324
325 CachedResponse {
326 status_code: code.to_string(),
327 description,
328 content_type,
329 schema,
330 }
331 }
332 ReferenceOr::Reference { .. } => CachedResponse {
333 status_code: code.to_string(),
334 description: None,
335 content_type: None,
336 schema: None,
337 },
338 }
339 })
340 .collect();
341
342 let security_requirements = operation.security.as_ref().map_or_else(
344 || global_security_requirements.to_vec(),
345 |security_reqs| {
346 security_reqs
347 .iter()
348 .flat_map(|security_req| security_req.keys().cloned())
349 .collect()
350 },
351 );
352
353 Ok(CachedCommand {
354 name,
355 description: operation.description.clone(),
356 summary: operation.summary.clone(),
357 operation_id,
358 method: method.to_uppercase(),
359 path: path.to_string(),
360 parameters,
361 request_body,
362 responses,
363 security_requirements,
364 tags: operation.tags.clone(),
365 deprecated: operation.deprecated,
366 external_docs_url: operation
367 .external_docs
368 .as_ref()
369 .map(|docs| docs.url.clone()),
370 })
371 }
372
373 #[allow(clippy::too_many_lines)]
375 fn transform_parameter(param: &Parameter) -> CachedParameter {
376 let (param_data, location_str) = match param {
377 Parameter::Query { parameter_data, .. } => (parameter_data, "query"),
378 Parameter::Header { parameter_data, .. } => (parameter_data, "header"),
379 Parameter::Path { parameter_data, .. } => (parameter_data, "path"),
380 Parameter::Cookie { parameter_data, .. } => (parameter_data, "cookie"),
381 };
382
383 let (schema_json, schema_type, format, default_value, enum_values) =
385 if let openapiv3::ParameterSchemaOrContent::Schema(schema_ref) = ¶m_data.format {
386 match schema_ref {
387 ReferenceOr::Item(schema) => {
388 let schema_json = serde_json::to_string(schema).ok();
389
390 let (schema_type, format, default, enums) = match &schema.schema_kind {
392 openapiv3::SchemaKind::Type(type_val) => match type_val {
393 openapiv3::Type::String(string_type) => {
394 let enum_values: Vec<String> = string_type
395 .enumeration
396 .iter()
397 .filter_map(|v| v.as_ref())
398 .map(|v| {
399 serde_json::to_string(v)
400 .unwrap_or_else(|_| v.to_string())
401 })
402 .collect();
403 let format = match &string_type.format {
404 openapiv3::VariantOrUnknownOrEmpty::Item(fmt) => {
405 Some(format!("{fmt:?}"))
406 }
407 _ => None,
408 };
409 ("string".to_string(), format, None, enum_values)
410 }
411 openapiv3::Type::Number(number_type) => {
412 let format = match &number_type.format {
413 openapiv3::VariantOrUnknownOrEmpty::Item(fmt) => {
414 Some(format!("{fmt:?}"))
415 }
416 _ => None,
417 };
418 ("number".to_string(), format, None, vec![])
419 }
420 openapiv3::Type::Integer(integer_type) => {
421 let format = match &integer_type.format {
422 openapiv3::VariantOrUnknownOrEmpty::Item(fmt) => {
423 Some(format!("{fmt:?}"))
424 }
425 _ => None,
426 };
427 ("integer".to_string(), format, None, vec![])
428 }
429 openapiv3::Type::Boolean(_) => {
430 ("boolean".to_string(), None, None, vec![])
431 }
432 openapiv3::Type::Array(_) => {
433 ("array".to_string(), None, None, vec![])
434 }
435 openapiv3::Type::Object(_) => {
436 ("object".to_string(), None, None, vec![])
437 }
438 },
439 _ => ("string".to_string(), None, None, vec![]),
440 };
441
442 let default_value =
444 schema.schema_data.default.as_ref().map(|v| {
445 serde_json::to_string(v).unwrap_or_else(|_| v.to_string())
446 });
447
448 (
449 schema_json,
450 Some(schema_type),
451 format,
452 default_value.or(default),
453 enums,
454 )
455 }
456 ReferenceOr::Reference { .. } => {
457 (
459 Some(r#"{"type": "string"}"#.to_string()),
460 Some("string".to_string()),
461 None,
462 None,
463 vec![],
464 )
465 }
466 }
467 } else {
468 (
470 Some(r#"{"type": "string"}"#.to_string()),
471 Some("string".to_string()),
472 None,
473 None,
474 vec![],
475 )
476 };
477
478 let example = param_data
480 .example
481 .as_ref()
482 .map(|ex| serde_json::to_string(ex).unwrap_or_else(|_| ex.to_string()));
483
484 CachedParameter {
485 name: param_data.name.clone(),
486 location: location_str.to_string(),
487 required: param_data.required,
488 description: param_data.description.clone(),
489 schema: schema_json,
490 schema_type,
491 format,
492 default_value,
493 enum_values,
494 example,
495 }
496 }
497
498 fn transform_request_body(
500 request_body: &ReferenceOr<RequestBody>,
501 ) -> Option<CachedRequestBody> {
502 match request_body {
503 ReferenceOr::Item(body) => {
504 let content_type = if body.content.contains_key("application/json") {
506 "application/json"
507 } else {
508 body.content.keys().next()?
509 };
510
511 let media_type = body.content.get(content_type)?;
513 let schema = media_type
514 .schema
515 .as_ref()
516 .and_then(|schema_ref| match schema_ref {
517 ReferenceOr::Item(schema) => serde_json::to_string(schema).ok(),
518 ReferenceOr::Reference { .. } => None,
519 })
520 .unwrap_or_else(|| "{}".to_string());
521
522 let example = media_type
523 .example
524 .as_ref()
525 .map(|ex| serde_json::to_string(ex).unwrap_or_else(|_| ex.to_string()));
526
527 Some(CachedRequestBody {
528 content_type: content_type.to_string(),
529 schema,
530 required: body.required,
531 description: body.description.clone(),
532 example,
533 })
534 }
535 ReferenceOr::Reference { .. } => None, }
537 }
538
539 fn extract_security_schemes(spec: &OpenAPI) -> HashMap<String, CachedSecurityScheme> {
541 let mut security_schemes = HashMap::new();
542
543 if let Some(components) = &spec.components {
544 for (name, scheme_ref) in &components.security_schemes {
545 if let ReferenceOr::Item(scheme) = scheme_ref {
546 if let Some(cached_scheme) = Self::transform_security_scheme(name, scheme) {
547 security_schemes.insert(name.clone(), cached_scheme);
548 }
549 }
550 }
551 }
552
553 security_schemes
554 }
555
556 fn transform_security_scheme(
558 name: &str,
559 scheme: &SecurityScheme,
560 ) -> Option<CachedSecurityScheme> {
561 match scheme {
562 SecurityScheme::APIKey {
563 location,
564 name: param_name,
565 description,
566 ..
567 } => {
568 let aperture_secret = Self::extract_aperture_secret(scheme);
569 let location_str = match location {
570 openapiv3::APIKeyLocation::Query => "query",
571 openapiv3::APIKeyLocation::Header => "header",
572 openapiv3::APIKeyLocation::Cookie => "cookie",
573 };
574
575 Some(CachedSecurityScheme {
576 name: name.to_string(),
577 scheme_type: "apiKey".to_string(),
578 scheme: None,
579 location: Some(location_str.to_string()),
580 parameter_name: Some(param_name.clone()),
581 description: description.clone(),
582 bearer_format: None,
583 aperture_secret,
584 })
585 }
586 SecurityScheme::HTTP {
587 scheme: http_scheme,
588 bearer_format,
589 description,
590 ..
591 } => {
592 let aperture_secret = Self::extract_aperture_secret(scheme);
593 Some(CachedSecurityScheme {
594 name: name.to_string(),
595 scheme_type: "http".to_string(),
596 scheme: Some(http_scheme.clone()),
597 location: Some("header".to_string()),
598 parameter_name: Some("Authorization".to_string()),
599 description: description.clone(),
600 bearer_format: bearer_format.clone(),
601 aperture_secret,
602 })
603 }
604 SecurityScheme::OAuth2 { .. } | SecurityScheme::OpenIDConnect { .. } => None,
606 }
607 }
608
609 fn extract_aperture_secret(scheme: &SecurityScheme) -> Option<CachedApertureSecret> {
611 let extensions = match scheme {
613 SecurityScheme::APIKey { extensions, .. } | SecurityScheme::HTTP { extensions, .. } => {
614 extensions
615 }
616 SecurityScheme::OAuth2 { .. } | SecurityScheme::OpenIDConnect { .. } => return None,
617 };
618
619 extensions.get("x-aperture-secret").and_then(|value| {
621 if let Some(obj) = value.as_object() {
623 let source = obj.get("source")?.as_str()?;
624 let name = obj.get("name")?.as_str()?;
625
626 if source == "env" {
628 return Some(CachedApertureSecret {
629 source: source.to_string(),
630 name: name.to_string(),
631 });
632 }
633 }
634 None
635 })
636 }
637
638 fn resolve_parameter_reference(spec: &OpenAPI, reference: &str) -> Result<Parameter, Error> {
640 crate::spec::resolve_parameter_reference(spec, reference)
641 }
642}
643
644impl Default for SpecTransformer {
645 fn default() -> Self {
646 Self::new()
647 }
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653 use openapiv3::{Info, OpenAPI};
654
655 fn create_test_spec() -> OpenAPI {
656 OpenAPI {
657 openapi: "3.0.0".to_string(),
658 info: Info {
659 title: "Test API".to_string(),
660 version: "1.0.0".to_string(),
661 ..Default::default()
662 },
663 servers: vec![openapiv3::Server {
664 url: "https://api.example.com".to_string(),
665 ..Default::default()
666 }],
667 paths: Default::default(),
668 ..Default::default()
669 }
670 }
671
672 #[test]
673 fn test_transform_basic_spec() {
674 let transformer = SpecTransformer::new();
675 let spec = create_test_spec();
676 let cached = transformer
677 .transform("test", &spec)
678 .expect("Transform should succeed");
679
680 assert_eq!(cached.name, "test");
681 assert_eq!(cached.version, "1.0.0");
682 assert_eq!(cached.base_url, Some("https://api.example.com".to_string()));
683 assert_eq!(cached.servers.len(), 1);
684 assert!(cached.commands.is_empty());
685 assert!(cached.server_variables.is_empty());
686 }
687
688 #[test]
689 fn test_transform_spec_with_server_variables() {
690 let mut variables = indexmap::IndexMap::new();
691 variables.insert(
692 "region".to_string(),
693 openapiv3::ServerVariable {
694 default: "us".to_string(),
695 description: Some("The regional instance".to_string()),
696 enumeration: vec!["us".to_string(), "eu".to_string()],
697 extensions: indexmap::IndexMap::new(),
698 },
699 );
700
701 let spec = OpenAPI {
702 openapi: "3.0.0".to_string(),
703 info: Info {
704 title: "Test API".to_string(),
705 version: "1.0.0".to_string(),
706 ..Default::default()
707 },
708 servers: vec![openapiv3::Server {
709 url: "https://{region}.api.example.com".to_string(),
710 description: Some("Regional server".to_string()),
711 variables: Some(variables),
712 extensions: indexmap::IndexMap::new(),
713 }],
714 ..Default::default()
715 };
716
717 let transformer = SpecTransformer::new();
718 let cached = transformer.transform("test", &spec).unwrap();
719
720 assert_eq!(cached.server_variables.len(), 1);
722 assert!(cached.server_variables.contains_key("region"));
723
724 let region_var = &cached.server_variables["region"];
725 assert_eq!(region_var.default, Some("us".to_string()));
726 assert_eq!(
727 region_var.description,
728 Some("The regional instance".to_string())
729 );
730 assert_eq!(
731 region_var.enum_values,
732 vec!["us".to_string(), "eu".to_string()]
733 );
734
735 assert_eq!(cached.name, "test");
737 assert_eq!(
738 cached.base_url,
739 Some("https://{region}.api.example.com".to_string())
740 );
741 }
742
743 #[test]
744 fn test_transform_spec_with_empty_default_server_variable() {
745 let mut variables = indexmap::IndexMap::new();
746 variables.insert(
747 "prefix".to_string(),
748 openapiv3::ServerVariable {
749 default: "".to_string(), description: Some("Optional prefix".to_string()),
751 enumeration: vec![],
752 extensions: indexmap::IndexMap::new(),
753 },
754 );
755
756 let spec = OpenAPI {
757 openapi: "3.0.0".to_string(),
758 info: Info {
759 title: "Test API".to_string(),
760 version: "1.0.0".to_string(),
761 ..Default::default()
762 },
763 servers: vec![openapiv3::Server {
764 url: "https://{prefix}api.example.com".to_string(),
765 description: Some("Server with empty default".to_string()),
766 variables: Some(variables),
767 extensions: indexmap::IndexMap::new(),
768 }],
769 ..Default::default()
770 };
771
772 let transformer = SpecTransformer::new();
773 let cached = transformer.transform("test", &spec).unwrap();
774
775 assert!(cached.server_variables.contains_key("prefix"));
777 let prefix_var = &cached.server_variables["prefix"];
778 assert_eq!(prefix_var.default, Some("".to_string()));
779 assert_eq!(prefix_var.description, Some("Optional prefix".to_string()));
780 }
781
782 #[test]
783 fn test_transform_with_operations() {
784 use openapiv3::{Operation, PathItem, ReferenceOr, Responses};
785
786 let transformer = SpecTransformer::new();
787 let mut spec = create_test_spec();
788
789 let mut path_item = PathItem::default();
790 path_item.get = Some(Operation {
791 operation_id: Some("getUsers".to_string()),
792 tags: vec!["users".to_string()],
793 description: Some("Get all users".to_string()),
794 responses: Responses::default(),
795 ..Default::default()
796 });
797
798 spec.paths
799 .paths
800 .insert("/users".to_string(), ReferenceOr::Item(path_item));
801
802 let cached = transformer
803 .transform("test", &spec)
804 .expect("Transform should succeed");
805
806 assert_eq!(cached.commands.len(), 1);
807 let command = &cached.commands[0];
808 assert_eq!(command.name, "users");
809 assert_eq!(command.operation_id, "getUsers");
810 assert_eq!(command.method, "GET");
811 assert_eq!(command.path, "/users");
812 assert_eq!(command.description, Some("Get all users".to_string()));
813 }
814
815 #[test]
816 fn test_transform_with_parameter_reference() {
817 use openapiv3::{
818 Components, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem,
819 ReferenceOr, Responses, Schema, SchemaData, SchemaKind, Type,
820 };
821
822 let transformer = SpecTransformer::new();
823 let mut spec = create_test_spec();
824
825 let mut components = Components::default();
827 let user_id_param = Parameter::Path {
828 parameter_data: ParameterData {
829 name: "userId".to_string(),
830 description: Some("Unique identifier of the user".to_string()),
831 required: true,
832 deprecated: Some(false),
833 format: ParameterSchemaOrContent::Schema(ReferenceOr::Item(Schema {
834 schema_data: SchemaData::default(),
835 schema_kind: SchemaKind::Type(Type::String(Default::default())),
836 })),
837 example: None,
838 examples: Default::default(),
839 explode: None,
840 extensions: Default::default(),
841 },
842 style: Default::default(),
843 };
844 components
845 .parameters
846 .insert("userId".to_string(), ReferenceOr::Item(user_id_param));
847 spec.components = Some(components);
848
849 let mut path_item = PathItem::default();
851 path_item.get = Some(Operation {
852 operation_id: Some("getUserById".to_string()),
853 tags: vec!["users".to_string()],
854 parameters: vec![ReferenceOr::Reference {
855 reference: "#/components/parameters/userId".to_string(),
856 }],
857 responses: Responses::default(),
858 ..Default::default()
859 });
860
861 spec.paths
862 .paths
863 .insert("/users/{userId}".to_string(), ReferenceOr::Item(path_item));
864
865 let cached = transformer
866 .transform("test", &spec)
867 .expect("Transform should succeed with parameter reference");
868
869 assert_eq!(cached.commands.len(), 1);
871 let command = &cached.commands[0];
872 assert_eq!(command.parameters.len(), 1);
873 let param = &command.parameters[0];
874 assert_eq!(param.name, "userId");
875 assert_eq!(param.location, "path");
876 assert!(param.required);
877 assert_eq!(
878 param.description,
879 Some("Unique identifier of the user".to_string())
880 );
881 }
882
883 #[test]
884 fn test_transform_with_invalid_parameter_reference() {
885 use openapiv3::{Operation, PathItem, ReferenceOr, Responses};
886
887 let transformer = SpecTransformer::new();
888 let mut spec = create_test_spec();
889
890 let mut path_item = PathItem::default();
892 path_item.get = Some(Operation {
893 parameters: vec![ReferenceOr::Reference {
894 reference: "#/invalid/reference/format".to_string(),
895 }],
896 responses: Responses::default(),
897 ..Default::default()
898 });
899
900 spec.paths
901 .paths
902 .insert("/users".to_string(), ReferenceOr::Item(path_item));
903
904 let result = transformer.transform("test", &spec);
905 assert!(result.is_err());
906 match result.unwrap_err() {
907 crate::error::Error::Validation(msg) => {
908 assert!(msg.contains("Invalid parameter reference format"));
909 }
910 _ => panic!("Expected Validation error"),
911 }
912 }
913
914 #[test]
915 fn test_transform_with_missing_parameter_reference() {
916 use openapiv3::{Components, Operation, PathItem, ReferenceOr, Responses};
917
918 let transformer = SpecTransformer::new();
919 let mut spec = create_test_spec();
920
921 spec.components = Some(Components::default());
923
924 let mut path_item = PathItem::default();
926 path_item.get = Some(Operation {
927 parameters: vec![ReferenceOr::Reference {
928 reference: "#/components/parameters/nonExistent".to_string(),
929 }],
930 responses: Responses::default(),
931 ..Default::default()
932 });
933
934 spec.paths
935 .paths
936 .insert("/users".to_string(), ReferenceOr::Item(path_item));
937
938 let result = transformer.transform("test", &spec);
939 assert!(result.is_err());
940 match result.unwrap_err() {
941 crate::error::Error::Validation(msg) => {
942 assert!(msg.contains("Parameter 'nonExistent' not found in components"));
943 }
944 _ => panic!("Expected Validation error"),
945 }
946 }
947
948 #[test]
949 fn test_transform_with_nested_parameter_reference() {
950 use openapiv3::{
951 Components, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem,
952 ReferenceOr, Responses, Schema, SchemaData, SchemaKind, Type,
953 };
954
955 let transformer = SpecTransformer::new();
956 let mut spec = create_test_spec();
957
958 let mut components = Components::default();
959
960 components.parameters.insert(
962 "userIdRef".to_string(),
963 ReferenceOr::Reference {
964 reference: "#/components/parameters/userId".to_string(),
965 },
966 );
967
968 let user_id_param = Parameter::Path {
970 parameter_data: ParameterData {
971 name: "userId".to_string(),
972 description: Some("User ID parameter".to_string()),
973 required: true,
974 deprecated: Some(false),
975 format: ParameterSchemaOrContent::Schema(ReferenceOr::Item(Schema {
976 schema_data: SchemaData::default(),
977 schema_kind: SchemaKind::Type(Type::String(Default::default())),
978 })),
979 example: None,
980 examples: Default::default(),
981 explode: None,
982 extensions: Default::default(),
983 },
984 style: Default::default(),
985 };
986 components
987 .parameters
988 .insert("userId".to_string(), ReferenceOr::Item(user_id_param));
989 spec.components = Some(components);
990
991 let mut path_item = PathItem::default();
993 path_item.get = Some(Operation {
994 parameters: vec![ReferenceOr::Reference {
995 reference: "#/components/parameters/userIdRef".to_string(),
996 }],
997 responses: Responses::default(),
998 ..Default::default()
999 });
1000
1001 spec.paths
1002 .paths
1003 .insert("/users/{userId}".to_string(), ReferenceOr::Item(path_item));
1004
1005 let cached = transformer
1006 .transform("test", &spec)
1007 .expect("Transform should succeed with nested parameter reference");
1008
1009 assert_eq!(cached.commands.len(), 1);
1011 let command = &cached.commands[0];
1012 assert_eq!(command.parameters.len(), 1);
1013 let param = &command.parameters[0];
1014 assert_eq!(param.name, "userId");
1015 assert_eq!(param.description, Some("User ID parameter".to_string()));
1016 }
1017
1018 #[test]
1019 fn test_transform_with_circular_parameter_reference() {
1020 use openapiv3::{Components, Operation, PathItem, ReferenceOr, Responses};
1021
1022 let transformer = SpecTransformer::new();
1023 let mut spec = create_test_spec();
1024
1025 let mut components = Components::default();
1026
1027 components.parameters.insert(
1029 "paramA".to_string(),
1030 ReferenceOr::Reference {
1031 reference: "#/components/parameters/paramA".to_string(),
1032 },
1033 );
1034
1035 spec.components = Some(components);
1036
1037 let mut path_item = PathItem::default();
1039 path_item.get = Some(Operation {
1040 parameters: vec![ReferenceOr::Reference {
1041 reference: "#/components/parameters/paramA".to_string(),
1042 }],
1043 responses: Responses::default(),
1044 ..Default::default()
1045 });
1046
1047 spec.paths
1048 .paths
1049 .insert("/test".to_string(), ReferenceOr::Item(path_item));
1050
1051 let result = transformer.transform("test", &spec);
1052 assert!(result.is_err());
1053 match result.unwrap_err() {
1054 crate::error::Error::Validation(msg) => {
1055 assert!(
1056 msg.contains("Circular reference detected"),
1057 "Error message should mention circular reference: {}",
1058 msg
1059 );
1060 }
1061 _ => panic!("Expected Validation error for circular reference"),
1062 }
1063 }
1064
1065 #[test]
1066 fn test_transform_with_indirect_circular_reference() {
1067 use openapiv3::{Components, Operation, PathItem, ReferenceOr, Responses};
1068
1069 let transformer = SpecTransformer::new();
1070 let mut spec = create_test_spec();
1071
1072 let mut components = Components::default();
1073
1074 components.parameters.insert(
1076 "paramA".to_string(),
1077 ReferenceOr::Reference {
1078 reference: "#/components/parameters/paramB".to_string(),
1079 },
1080 );
1081
1082 components.parameters.insert(
1083 "paramB".to_string(),
1084 ReferenceOr::Reference {
1085 reference: "#/components/parameters/paramA".to_string(),
1086 },
1087 );
1088
1089 spec.components = Some(components);
1090
1091 let mut path_item = PathItem::default();
1093 path_item.get = Some(Operation {
1094 parameters: vec![ReferenceOr::Reference {
1095 reference: "#/components/parameters/paramA".to_string(),
1096 }],
1097 responses: Responses::default(),
1098 ..Default::default()
1099 });
1100
1101 spec.paths
1102 .paths
1103 .insert("/test".to_string(), ReferenceOr::Item(path_item));
1104
1105 let result = transformer.transform("test", &spec);
1106 assert!(result.is_err());
1107 match result.unwrap_err() {
1108 crate::error::Error::Validation(msg) => {
1109 assert!(
1110 msg.contains("Circular reference detected") || msg.contains("reference cycle"),
1111 "Error message should mention circular reference: {}",
1112 msg
1113 );
1114 }
1115 _ => panic!("Expected Validation error for circular reference"),
1116 }
1117 }
1118
1119 #[test]
1120 fn test_transform_with_complex_circular_reference() {
1121 use openapiv3::{Components, Operation, PathItem, ReferenceOr, Responses};
1122
1123 let transformer = SpecTransformer::new();
1124 let mut spec = create_test_spec();
1125
1126 let mut components = Components::default();
1127
1128 components.parameters.insert(
1130 "paramA".to_string(),
1131 ReferenceOr::Reference {
1132 reference: "#/components/parameters/paramB".to_string(),
1133 },
1134 );
1135
1136 components.parameters.insert(
1137 "paramB".to_string(),
1138 ReferenceOr::Reference {
1139 reference: "#/components/parameters/paramC".to_string(),
1140 },
1141 );
1142
1143 components.parameters.insert(
1144 "paramC".to_string(),
1145 ReferenceOr::Reference {
1146 reference: "#/components/parameters/paramA".to_string(),
1147 },
1148 );
1149
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/paramA".to_string(),
1157 }],
1158 responses: Responses::default(),
1159 ..Default::default()
1160 });
1161
1162 spec.paths
1163 .paths
1164 .insert("/test".to_string(), ReferenceOr::Item(path_item));
1165
1166 let result = transformer.transform("test", &spec);
1167 assert!(result.is_err());
1168 match result.unwrap_err() {
1169 crate::error::Error::Validation(msg) => {
1170 assert!(
1171 msg.contains("Circular reference detected") || msg.contains("reference cycle"),
1172 "Error message should mention circular reference: {}",
1173 msg
1174 );
1175 }
1176 _ => panic!("Expected Validation error for circular reference"),
1177 }
1178 }
1179
1180 #[test]
1181 fn test_transform_with_depth_limit() {
1182 use openapiv3::{
1183 Components, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem,
1184 ReferenceOr, Responses, Schema, SchemaData, SchemaKind, Type,
1185 };
1186
1187 let transformer = SpecTransformer::new();
1188 let mut spec = create_test_spec();
1189
1190 let mut components = Components::default();
1191
1192 for i in 0..12 {
1194 let param_name = format!("param{}", i);
1195 let next_param = format!("param{}", i + 1);
1196
1197 if i < 11 {
1198 components.parameters.insert(
1200 param_name,
1201 ReferenceOr::Reference {
1202 reference: format!("#/components/parameters/{}", next_param),
1203 },
1204 );
1205 } else {
1206 let actual_param = Parameter::Path {
1208 parameter_data: ParameterData {
1209 name: "deepParam".to_string(),
1210 description: Some("Very deeply nested parameter".to_string()),
1211 required: true,
1212 deprecated: Some(false),
1213 format: ParameterSchemaOrContent::Schema(ReferenceOr::Item(Schema {
1214 schema_data: SchemaData::default(),
1215 schema_kind: SchemaKind::Type(Type::String(Default::default())),
1216 })),
1217 example: None,
1218 examples: Default::default(),
1219 explode: None,
1220 extensions: Default::default(),
1221 },
1222 style: Default::default(),
1223 };
1224 components
1225 .parameters
1226 .insert(param_name, ReferenceOr::Item(actual_param));
1227 }
1228 }
1229
1230 spec.components = Some(components);
1231
1232 let mut path_item = PathItem::default();
1234 path_item.get = Some(Operation {
1235 parameters: vec![ReferenceOr::Reference {
1236 reference: "#/components/parameters/param0".to_string(),
1237 }],
1238 responses: Responses::default(),
1239 ..Default::default()
1240 });
1241
1242 spec.paths
1243 .paths
1244 .insert("/test".to_string(), ReferenceOr::Item(path_item));
1245
1246 let result = transformer.transform("test", &spec);
1247 assert!(result.is_err());
1248 match result.unwrap_err() {
1249 crate::error::Error::Validation(msg) => {
1250 assert!(
1251 msg.contains("Maximum reference depth") && msg.contains("10"),
1252 "Error message should mention depth limit: {}",
1253 msg
1254 );
1255 }
1256 _ => panic!("Expected Validation error for depth limit"),
1257 }
1258 }
1259}