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
10pub struct SpecTransformer;
12
13impl SpecTransformer {
14 #[must_use]
16 pub const fn new() -> Self {
17 Self
18 }
19
20 pub fn transform(&self, name: &str, spec: &OpenAPI) -> Result<CachedSpec, Error> {
29 self.transform_with_filter(name, spec, &[])
30 }
31
32 pub fn transform_with_filter(
47 &self,
48 name: &str,
49 spec: &OpenAPI,
50 skip_endpoints: &[(String, String)],
51 ) -> Result<CachedSpec, Error> {
52 self.transform_with_warnings(name, spec, skip_endpoints, &[])
53 }
54
55 pub fn transform_with_warnings(
68 &self,
69 name: &str,
70 spec: &OpenAPI,
71 skip_endpoints: &[(String, String)],
72 warnings: &[crate::spec::validator::ValidationWarning],
73 ) -> Result<CachedSpec, Error> {
74 let mut commands = Vec::new();
75
76 let version = spec.info.version.clone();
78
79 let servers: Vec<String> = spec.servers.iter().map(|s| s.url.clone()).collect();
81 let base_url = servers.first().cloned();
82
83 let global_security_requirements: Vec<String> = spec
85 .security
86 .iter()
87 .flat_map(|security_vec| {
88 security_vec
89 .iter()
90 .flat_map(|security_req| security_req.keys().cloned())
91 })
92 .collect();
93
94 for (path, path_item) in spec.paths.iter() {
96 Self::process_path_item(
97 spec,
98 path,
99 path_item,
100 skip_endpoints,
101 &global_security_requirements,
102 &mut commands,
103 )?;
104 }
105
106 let security_schemes = Self::extract_security_schemes(spec);
108
109 let skipped_endpoints: Vec<SkippedEndpoint> = warnings
111 .iter()
112 .map(|w| SkippedEndpoint {
113 path: w.endpoint.path.clone(),
114 method: w.endpoint.method.clone(),
115 content_type: w.endpoint.content_type.clone(),
116 reason: w.reason.clone(),
117 })
118 .collect();
119
120 Ok(CachedSpec {
121 cache_format_version: CACHE_FORMAT_VERSION,
122 name: name.to_string(),
123 version,
124 commands,
125 base_url,
126 servers,
127 security_schemes,
128 skipped_endpoints,
129 })
130 }
131
132 fn process_path_item(
134 spec: &OpenAPI,
135 path: &str,
136 path_item: &ReferenceOr<openapiv3::PathItem>,
137 skip_endpoints: &[(String, String)],
138 global_security_requirements: &[String],
139 commands: &mut Vec<CachedCommand>,
140 ) -> Result<(), Error> {
141 let ReferenceOr::Item(item) = path_item else {
142 return Ok(());
143 };
144
145 for (method, operation) in crate::spec::http_methods_iter(item) {
147 let Some(op) = operation else {
148 continue;
149 };
150
151 if Self::should_skip_endpoint(path, method, skip_endpoints) {
152 continue;
153 }
154
155 let command =
156 Self::transform_operation(spec, method, path, op, global_security_requirements)?;
157 commands.push(command);
158 }
159
160 Ok(())
161 }
162
163 fn should_skip_endpoint(path: &str, method: &str, skip_endpoints: &[(String, String)]) -> bool {
165 skip_endpoints.iter().any(|(skip_path, skip_method)| {
166 skip_path == path && skip_method.eq_ignore_ascii_case(method)
167 })
168 }
169
170 fn transform_operation(
172 spec: &OpenAPI,
173 method: &str,
174 path: &str,
175 operation: &Operation,
176 global_security_requirements: &[String],
177 ) -> Result<CachedCommand, Error> {
178 let operation_id = operation
180 .operation_id
181 .clone()
182 .unwrap_or_else(|| format!("{method}_{path}"));
183
184 let name = operation
186 .tags
187 .first()
188 .cloned()
189 .unwrap_or_else(|| "default".to_string());
190
191 let mut parameters = Vec::new();
193 for param_ref in &operation.parameters {
194 match param_ref {
195 ReferenceOr::Item(param) => {
196 parameters.push(Self::transform_parameter(param));
197 }
198 ReferenceOr::Reference { reference } => {
199 let param = Self::resolve_parameter_reference(spec, reference)?;
200 parameters.push(Self::transform_parameter(¶m));
201 }
202 }
203 }
204
205 let request_body = operation
207 .request_body
208 .as_ref()
209 .and_then(Self::transform_request_body);
210
211 let responses = operation
213 .responses
214 .responses
215 .iter()
216 .map(|(code, response_ref)| {
217 match response_ref {
218 ReferenceOr::Item(response) => {
219 let description = if response.description.is_empty() {
221 None
222 } else {
223 Some(response.description.clone())
224 };
225
226 let (content_type, schema) =
228 if let Some((ct, media_type)) = response.content.iter().next() {
229 let schema = media_type.schema.as_ref().and_then(|schema_ref| {
230 match schema_ref {
231 ReferenceOr::Item(schema) => {
232 serde_json::to_string(schema).ok()
233 }
234 ReferenceOr::Reference { .. } => None,
235 }
236 });
237 (Some(ct.clone()), schema)
238 } else {
239 (None, None)
240 };
241
242 CachedResponse {
243 status_code: code.to_string(),
244 description,
245 content_type,
246 schema,
247 }
248 }
249 ReferenceOr::Reference { .. } => CachedResponse {
250 status_code: code.to_string(),
251 description: None,
252 content_type: None,
253 schema: None,
254 },
255 }
256 })
257 .collect();
258
259 let security_requirements = operation.security.as_ref().map_or_else(
261 || global_security_requirements.to_vec(),
262 |security_reqs| {
263 security_reqs
264 .iter()
265 .flat_map(|security_req| security_req.keys().cloned())
266 .collect()
267 },
268 );
269
270 Ok(CachedCommand {
271 name,
272 description: operation.description.clone(),
273 summary: operation.summary.clone(),
274 operation_id,
275 method: method.to_uppercase(),
276 path: path.to_string(),
277 parameters,
278 request_body,
279 responses,
280 security_requirements,
281 tags: operation.tags.clone(),
282 deprecated: operation.deprecated,
283 external_docs_url: operation
284 .external_docs
285 .as_ref()
286 .map(|docs| docs.url.clone()),
287 })
288 }
289
290 #[allow(clippy::too_many_lines)]
292 fn transform_parameter(param: &Parameter) -> CachedParameter {
293 let (param_data, location_str) = match param {
294 Parameter::Query { parameter_data, .. } => (parameter_data, "query"),
295 Parameter::Header { parameter_data, .. } => (parameter_data, "header"),
296 Parameter::Path { parameter_data, .. } => (parameter_data, "path"),
297 Parameter::Cookie { parameter_data, .. } => (parameter_data, "cookie"),
298 };
299
300 let (schema_json, schema_type, format, default_value, enum_values) =
302 if let openapiv3::ParameterSchemaOrContent::Schema(schema_ref) = ¶m_data.format {
303 match schema_ref {
304 ReferenceOr::Item(schema) => {
305 let schema_json = serde_json::to_string(schema).ok();
306
307 let (schema_type, format, default, enums) = match &schema.schema_kind {
309 openapiv3::SchemaKind::Type(type_val) => match type_val {
310 openapiv3::Type::String(string_type) => {
311 let enum_values: Vec<String> = string_type
312 .enumeration
313 .iter()
314 .filter_map(|v| v.as_ref())
315 .map(|v| {
316 serde_json::to_string(v)
317 .unwrap_or_else(|_| v.to_string())
318 })
319 .collect();
320 let format = match &string_type.format {
321 openapiv3::VariantOrUnknownOrEmpty::Item(fmt) => {
322 Some(format!("{fmt:?}"))
323 }
324 _ => None,
325 };
326 ("string".to_string(), format, None, enum_values)
327 }
328 openapiv3::Type::Number(number_type) => {
329 let format = match &number_type.format {
330 openapiv3::VariantOrUnknownOrEmpty::Item(fmt) => {
331 Some(format!("{fmt:?}"))
332 }
333 _ => None,
334 };
335 ("number".to_string(), format, None, vec![])
336 }
337 openapiv3::Type::Integer(integer_type) => {
338 let format = match &integer_type.format {
339 openapiv3::VariantOrUnknownOrEmpty::Item(fmt) => {
340 Some(format!("{fmt:?}"))
341 }
342 _ => None,
343 };
344 ("integer".to_string(), format, None, vec![])
345 }
346 openapiv3::Type::Boolean(_) => {
347 ("boolean".to_string(), None, None, vec![])
348 }
349 openapiv3::Type::Array(_) => {
350 ("array".to_string(), None, None, vec![])
351 }
352 openapiv3::Type::Object(_) => {
353 ("object".to_string(), None, None, vec![])
354 }
355 },
356 _ => ("string".to_string(), None, None, vec![]),
357 };
358
359 let default_value =
361 schema.schema_data.default.as_ref().map(|v| {
362 serde_json::to_string(v).unwrap_or_else(|_| v.to_string())
363 });
364
365 (
366 schema_json,
367 Some(schema_type),
368 format,
369 default_value.or(default),
370 enums,
371 )
372 }
373 ReferenceOr::Reference { .. } => {
374 (
376 Some(r#"{"type": "string"}"#.to_string()),
377 Some("string".to_string()),
378 None,
379 None,
380 vec![],
381 )
382 }
383 }
384 } else {
385 (
387 Some(r#"{"type": "string"}"#.to_string()),
388 Some("string".to_string()),
389 None,
390 None,
391 vec![],
392 )
393 };
394
395 let example = param_data
397 .example
398 .as_ref()
399 .map(|ex| serde_json::to_string(ex).unwrap_or_else(|_| ex.to_string()));
400
401 CachedParameter {
402 name: param_data.name.clone(),
403 location: location_str.to_string(),
404 required: param_data.required,
405 description: param_data.description.clone(),
406 schema: schema_json,
407 schema_type,
408 format,
409 default_value,
410 enum_values,
411 example,
412 }
413 }
414
415 fn transform_request_body(
417 request_body: &ReferenceOr<RequestBody>,
418 ) -> Option<CachedRequestBody> {
419 match request_body {
420 ReferenceOr::Item(body) => {
421 let content_type = if body.content.contains_key("application/json") {
423 "application/json"
424 } else {
425 body.content.keys().next()?
426 };
427
428 let media_type = body.content.get(content_type)?;
430 let schema = media_type
431 .schema
432 .as_ref()
433 .and_then(|schema_ref| match schema_ref {
434 ReferenceOr::Item(schema) => serde_json::to_string(schema).ok(),
435 ReferenceOr::Reference { .. } => None,
436 })
437 .unwrap_or_else(|| "{}".to_string());
438
439 let example = media_type
440 .example
441 .as_ref()
442 .map(|ex| serde_json::to_string(ex).unwrap_or_else(|_| ex.to_string()));
443
444 Some(CachedRequestBody {
445 content_type: content_type.to_string(),
446 schema,
447 required: body.required,
448 description: body.description.clone(),
449 example,
450 })
451 }
452 ReferenceOr::Reference { .. } => None, }
454 }
455
456 fn extract_security_schemes(spec: &OpenAPI) -> HashMap<String, CachedSecurityScheme> {
458 let mut security_schemes = HashMap::new();
459
460 if let Some(components) = &spec.components {
461 for (name, scheme_ref) in &components.security_schemes {
462 if let ReferenceOr::Item(scheme) = scheme_ref {
463 if let Some(cached_scheme) = Self::transform_security_scheme(name, scheme) {
464 security_schemes.insert(name.clone(), cached_scheme);
465 }
466 }
467 }
468 }
469
470 security_schemes
471 }
472
473 fn transform_security_scheme(
475 name: &str,
476 scheme: &SecurityScheme,
477 ) -> Option<CachedSecurityScheme> {
478 match scheme {
479 SecurityScheme::APIKey {
480 location,
481 name: param_name,
482 description,
483 ..
484 } => {
485 let aperture_secret = Self::extract_aperture_secret(scheme);
486 let location_str = match location {
487 openapiv3::APIKeyLocation::Query => "query",
488 openapiv3::APIKeyLocation::Header => "header",
489 openapiv3::APIKeyLocation::Cookie => "cookie",
490 };
491
492 Some(CachedSecurityScheme {
493 name: name.to_string(),
494 scheme_type: "apiKey".to_string(),
495 scheme: None,
496 location: Some(location_str.to_string()),
497 parameter_name: Some(param_name.clone()),
498 description: description.clone(),
499 bearer_format: None,
500 aperture_secret,
501 })
502 }
503 SecurityScheme::HTTP {
504 scheme: http_scheme,
505 bearer_format,
506 description,
507 ..
508 } => {
509 let aperture_secret = Self::extract_aperture_secret(scheme);
510 Some(CachedSecurityScheme {
511 name: name.to_string(),
512 scheme_type: "http".to_string(),
513 scheme: Some(http_scheme.clone()),
514 location: Some("header".to_string()),
515 parameter_name: Some("Authorization".to_string()),
516 description: description.clone(),
517 bearer_format: bearer_format.clone(),
518 aperture_secret,
519 })
520 }
521 SecurityScheme::OAuth2 { .. } | SecurityScheme::OpenIDConnect { .. } => None,
523 }
524 }
525
526 fn extract_aperture_secret(scheme: &SecurityScheme) -> Option<CachedApertureSecret> {
528 let extensions = match scheme {
530 SecurityScheme::APIKey { extensions, .. } | SecurityScheme::HTTP { extensions, .. } => {
531 extensions
532 }
533 SecurityScheme::OAuth2 { .. } | SecurityScheme::OpenIDConnect { .. } => return None,
534 };
535
536 extensions.get("x-aperture-secret").and_then(|value| {
538 if let Some(obj) = value.as_object() {
540 let source = obj.get("source")?.as_str()?;
541 let name = obj.get("name")?.as_str()?;
542
543 if source == "env" {
545 return Some(CachedApertureSecret {
546 source: source.to_string(),
547 name: name.to_string(),
548 });
549 }
550 }
551 None
552 })
553 }
554
555 fn resolve_parameter_reference(spec: &OpenAPI, reference: &str) -> Result<Parameter, Error> {
557 crate::spec::resolve_parameter_reference(spec, reference)
558 }
559}
560
561impl Default for SpecTransformer {
562 fn default() -> Self {
563 Self::new()
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570 use openapiv3::{Info, OpenAPI};
571
572 fn create_test_spec() -> OpenAPI {
573 OpenAPI {
574 openapi: "3.0.0".to_string(),
575 info: Info {
576 title: "Test API".to_string(),
577 version: "1.0.0".to_string(),
578 ..Default::default()
579 },
580 servers: vec![openapiv3::Server {
581 url: "https://api.example.com".to_string(),
582 ..Default::default()
583 }],
584 paths: Default::default(),
585 ..Default::default()
586 }
587 }
588
589 #[test]
590 fn test_transform_basic_spec() {
591 let transformer = SpecTransformer::new();
592 let spec = create_test_spec();
593 let cached = transformer
594 .transform("test", &spec)
595 .expect("Transform should succeed");
596
597 assert_eq!(cached.name, "test");
598 assert_eq!(cached.version, "1.0.0");
599 assert_eq!(cached.base_url, Some("https://api.example.com".to_string()));
600 assert_eq!(cached.servers.len(), 1);
601 assert!(cached.commands.is_empty());
602 }
603
604 #[test]
605 fn test_transform_with_operations() {
606 use openapiv3::{Operation, PathItem, ReferenceOr, Responses};
607
608 let transformer = SpecTransformer::new();
609 let mut spec = create_test_spec();
610
611 let mut path_item = PathItem::default();
612 path_item.get = Some(Operation {
613 operation_id: Some("getUsers".to_string()),
614 tags: vec!["users".to_string()],
615 description: Some("Get all users".to_string()),
616 responses: Responses::default(),
617 ..Default::default()
618 });
619
620 spec.paths
621 .paths
622 .insert("/users".to_string(), ReferenceOr::Item(path_item));
623
624 let cached = transformer
625 .transform("test", &spec)
626 .expect("Transform should succeed");
627
628 assert_eq!(cached.commands.len(), 1);
629 let command = &cached.commands[0];
630 assert_eq!(command.name, "users");
631 assert_eq!(command.operation_id, "getUsers");
632 assert_eq!(command.method, "GET");
633 assert_eq!(command.path, "/users");
634 assert_eq!(command.description, Some("Get all users".to_string()));
635 }
636
637 #[test]
638 fn test_transform_with_parameter_reference() {
639 use openapiv3::{
640 Components, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem,
641 ReferenceOr, Responses, Schema, SchemaData, SchemaKind, Type,
642 };
643
644 let transformer = SpecTransformer::new();
645 let mut spec = create_test_spec();
646
647 let mut components = Components::default();
649 let user_id_param = Parameter::Path {
650 parameter_data: ParameterData {
651 name: "userId".to_string(),
652 description: Some("Unique identifier of the user".to_string()),
653 required: true,
654 deprecated: Some(false),
655 format: ParameterSchemaOrContent::Schema(ReferenceOr::Item(Schema {
656 schema_data: SchemaData::default(),
657 schema_kind: SchemaKind::Type(Type::String(Default::default())),
658 })),
659 example: None,
660 examples: Default::default(),
661 explode: None,
662 extensions: Default::default(),
663 },
664 style: Default::default(),
665 };
666 components
667 .parameters
668 .insert("userId".to_string(), ReferenceOr::Item(user_id_param));
669 spec.components = Some(components);
670
671 let mut path_item = PathItem::default();
673 path_item.get = Some(Operation {
674 operation_id: Some("getUserById".to_string()),
675 tags: vec!["users".to_string()],
676 parameters: vec![ReferenceOr::Reference {
677 reference: "#/components/parameters/userId".to_string(),
678 }],
679 responses: Responses::default(),
680 ..Default::default()
681 });
682
683 spec.paths
684 .paths
685 .insert("/users/{userId}".to_string(), ReferenceOr::Item(path_item));
686
687 let cached = transformer
688 .transform("test", &spec)
689 .expect("Transform should succeed with parameter reference");
690
691 assert_eq!(cached.commands.len(), 1);
693 let command = &cached.commands[0];
694 assert_eq!(command.parameters.len(), 1);
695 let param = &command.parameters[0];
696 assert_eq!(param.name, "userId");
697 assert_eq!(param.location, "path");
698 assert!(param.required);
699 assert_eq!(
700 param.description,
701 Some("Unique identifier of the user".to_string())
702 );
703 }
704
705 #[test]
706 fn test_transform_with_invalid_parameter_reference() {
707 use openapiv3::{Operation, PathItem, ReferenceOr, Responses};
708
709 let transformer = SpecTransformer::new();
710 let mut spec = create_test_spec();
711
712 let mut path_item = PathItem::default();
714 path_item.get = Some(Operation {
715 parameters: vec![ReferenceOr::Reference {
716 reference: "#/invalid/reference/format".to_string(),
717 }],
718 responses: Responses::default(),
719 ..Default::default()
720 });
721
722 spec.paths
723 .paths
724 .insert("/users".to_string(), ReferenceOr::Item(path_item));
725
726 let result = transformer.transform("test", &spec);
727 assert!(result.is_err());
728 match result.unwrap_err() {
729 crate::error::Error::Validation(msg) => {
730 assert!(msg.contains("Invalid parameter reference format"));
731 }
732 _ => panic!("Expected Validation error"),
733 }
734 }
735
736 #[test]
737 fn test_transform_with_missing_parameter_reference() {
738 use openapiv3::{Components, Operation, PathItem, ReferenceOr, Responses};
739
740 let transformer = SpecTransformer::new();
741 let mut spec = create_test_spec();
742
743 spec.components = Some(Components::default());
745
746 let mut path_item = PathItem::default();
748 path_item.get = Some(Operation {
749 parameters: vec![ReferenceOr::Reference {
750 reference: "#/components/parameters/nonExistent".to_string(),
751 }],
752 responses: Responses::default(),
753 ..Default::default()
754 });
755
756 spec.paths
757 .paths
758 .insert("/users".to_string(), ReferenceOr::Item(path_item));
759
760 let result = transformer.transform("test", &spec);
761 assert!(result.is_err());
762 match result.unwrap_err() {
763 crate::error::Error::Validation(msg) => {
764 assert!(msg.contains("Parameter 'nonExistent' not found in components"));
765 }
766 _ => panic!("Expected Validation error"),
767 }
768 }
769
770 #[test]
771 fn test_transform_with_nested_parameter_reference() {
772 use openapiv3::{
773 Components, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem,
774 ReferenceOr, Responses, Schema, SchemaData, SchemaKind, Type,
775 };
776
777 let transformer = SpecTransformer::new();
778 let mut spec = create_test_spec();
779
780 let mut components = Components::default();
781
782 components.parameters.insert(
784 "userIdRef".to_string(),
785 ReferenceOr::Reference {
786 reference: "#/components/parameters/userId".to_string(),
787 },
788 );
789
790 let user_id_param = Parameter::Path {
792 parameter_data: ParameterData {
793 name: "userId".to_string(),
794 description: Some("User ID parameter".to_string()),
795 required: true,
796 deprecated: Some(false),
797 format: ParameterSchemaOrContent::Schema(ReferenceOr::Item(Schema {
798 schema_data: SchemaData::default(),
799 schema_kind: SchemaKind::Type(Type::String(Default::default())),
800 })),
801 example: None,
802 examples: Default::default(),
803 explode: None,
804 extensions: Default::default(),
805 },
806 style: Default::default(),
807 };
808 components
809 .parameters
810 .insert("userId".to_string(), ReferenceOr::Item(user_id_param));
811 spec.components = Some(components);
812
813 let mut path_item = PathItem::default();
815 path_item.get = Some(Operation {
816 parameters: vec![ReferenceOr::Reference {
817 reference: "#/components/parameters/userIdRef".to_string(),
818 }],
819 responses: Responses::default(),
820 ..Default::default()
821 });
822
823 spec.paths
824 .paths
825 .insert("/users/{userId}".to_string(), ReferenceOr::Item(path_item));
826
827 let cached = transformer
828 .transform("test", &spec)
829 .expect("Transform should succeed with nested parameter reference");
830
831 assert_eq!(cached.commands.len(), 1);
833 let command = &cached.commands[0];
834 assert_eq!(command.parameters.len(), 1);
835 let param = &command.parameters[0];
836 assert_eq!(param.name, "userId");
837 assert_eq!(param.description, Some("User ID parameter".to_string()));
838 }
839
840 #[test]
841 fn test_transform_with_circular_parameter_reference() {
842 use openapiv3::{Components, Operation, PathItem, ReferenceOr, Responses};
843
844 let transformer = SpecTransformer::new();
845 let mut spec = create_test_spec();
846
847 let mut components = Components::default();
848
849 components.parameters.insert(
851 "paramA".to_string(),
852 ReferenceOr::Reference {
853 reference: "#/components/parameters/paramA".to_string(),
854 },
855 );
856
857 spec.components = Some(components);
858
859 let mut path_item = PathItem::default();
861 path_item.get = Some(Operation {
862 parameters: vec![ReferenceOr::Reference {
863 reference: "#/components/parameters/paramA".to_string(),
864 }],
865 responses: Responses::default(),
866 ..Default::default()
867 });
868
869 spec.paths
870 .paths
871 .insert("/test".to_string(), ReferenceOr::Item(path_item));
872
873 let result = transformer.transform("test", &spec);
874 assert!(result.is_err());
875 match result.unwrap_err() {
876 crate::error::Error::Validation(msg) => {
877 assert!(
878 msg.contains("Circular reference detected"),
879 "Error message should mention circular reference: {}",
880 msg
881 );
882 }
883 _ => panic!("Expected Validation error for circular reference"),
884 }
885 }
886
887 #[test]
888 fn test_transform_with_indirect_circular_reference() {
889 use openapiv3::{Components, Operation, PathItem, ReferenceOr, Responses};
890
891 let transformer = SpecTransformer::new();
892 let mut spec = create_test_spec();
893
894 let mut components = Components::default();
895
896 components.parameters.insert(
898 "paramA".to_string(),
899 ReferenceOr::Reference {
900 reference: "#/components/parameters/paramB".to_string(),
901 },
902 );
903
904 components.parameters.insert(
905 "paramB".to_string(),
906 ReferenceOr::Reference {
907 reference: "#/components/parameters/paramA".to_string(),
908 },
909 );
910
911 spec.components = Some(components);
912
913 let mut path_item = PathItem::default();
915 path_item.get = Some(Operation {
916 parameters: vec![ReferenceOr::Reference {
917 reference: "#/components/parameters/paramA".to_string(),
918 }],
919 responses: Responses::default(),
920 ..Default::default()
921 });
922
923 spec.paths
924 .paths
925 .insert("/test".to_string(), ReferenceOr::Item(path_item));
926
927 let result = transformer.transform("test", &spec);
928 assert!(result.is_err());
929 match result.unwrap_err() {
930 crate::error::Error::Validation(msg) => {
931 assert!(
932 msg.contains("Circular reference detected") || msg.contains("reference cycle"),
933 "Error message should mention circular reference: {}",
934 msg
935 );
936 }
937 _ => panic!("Expected Validation error for circular reference"),
938 }
939 }
940
941 #[test]
942 fn test_transform_with_complex_circular_reference() {
943 use openapiv3::{Components, Operation, PathItem, ReferenceOr, Responses};
944
945 let transformer = SpecTransformer::new();
946 let mut spec = create_test_spec();
947
948 let mut components = Components::default();
949
950 components.parameters.insert(
952 "paramA".to_string(),
953 ReferenceOr::Reference {
954 reference: "#/components/parameters/paramB".to_string(),
955 },
956 );
957
958 components.parameters.insert(
959 "paramB".to_string(),
960 ReferenceOr::Reference {
961 reference: "#/components/parameters/paramC".to_string(),
962 },
963 );
964
965 components.parameters.insert(
966 "paramC".to_string(),
967 ReferenceOr::Reference {
968 reference: "#/components/parameters/paramA".to_string(),
969 },
970 );
971
972 spec.components = Some(components);
973
974 let mut path_item = PathItem::default();
976 path_item.get = Some(Operation {
977 parameters: vec![ReferenceOr::Reference {
978 reference: "#/components/parameters/paramA".to_string(),
979 }],
980 responses: Responses::default(),
981 ..Default::default()
982 });
983
984 spec.paths
985 .paths
986 .insert("/test".to_string(), ReferenceOr::Item(path_item));
987
988 let result = transformer.transform("test", &spec);
989 assert!(result.is_err());
990 match result.unwrap_err() {
991 crate::error::Error::Validation(msg) => {
992 assert!(
993 msg.contains("Circular reference detected") || msg.contains("reference cycle"),
994 "Error message should mention circular reference: {}",
995 msg
996 );
997 }
998 _ => panic!("Expected Validation error for circular reference"),
999 }
1000 }
1001
1002 #[test]
1003 fn test_transform_with_depth_limit() {
1004 use openapiv3::{
1005 Components, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem,
1006 ReferenceOr, Responses, Schema, SchemaData, SchemaKind, Type,
1007 };
1008
1009 let transformer = SpecTransformer::new();
1010 let mut spec = create_test_spec();
1011
1012 let mut components = Components::default();
1013
1014 for i in 0..12 {
1016 let param_name = format!("param{}", i);
1017 let next_param = format!("param{}", i + 1);
1018
1019 if i < 11 {
1020 components.parameters.insert(
1022 param_name,
1023 ReferenceOr::Reference {
1024 reference: format!("#/components/parameters/{}", next_param),
1025 },
1026 );
1027 } else {
1028 let actual_param = Parameter::Path {
1030 parameter_data: ParameterData {
1031 name: "deepParam".to_string(),
1032 description: Some("Very deeply nested parameter".to_string()),
1033 required: true,
1034 deprecated: Some(false),
1035 format: ParameterSchemaOrContent::Schema(ReferenceOr::Item(Schema {
1036 schema_data: SchemaData::default(),
1037 schema_kind: SchemaKind::Type(Type::String(Default::default())),
1038 })),
1039 example: None,
1040 examples: Default::default(),
1041 explode: None,
1042 extensions: Default::default(),
1043 },
1044 style: Default::default(),
1045 };
1046 components
1047 .parameters
1048 .insert(param_name, ReferenceOr::Item(actual_param));
1049 }
1050 }
1051
1052 spec.components = Some(components);
1053
1054 let mut path_item = PathItem::default();
1056 path_item.get = Some(Operation {
1057 parameters: vec![ReferenceOr::Reference {
1058 reference: "#/components/parameters/param0".to_string(),
1059 }],
1060 responses: Responses::default(),
1061 ..Default::default()
1062 });
1063
1064 spec.paths
1065 .paths
1066 .insert("/test".to_string(), ReferenceOr::Item(path_item));
1067
1068 let result = transformer.transform("test", &spec);
1069 assert!(result.is_err());
1070 match result.unwrap_err() {
1071 crate::error::Error::Validation(msg) => {
1072 assert!(
1073 msg.contains("Maximum reference depth") && msg.contains("10"),
1074 "Error message should mention depth limit: {}",
1075 msg
1076 );
1077 }
1078 _ => panic!("Expected Validation error for depth limit"),
1079 }
1080 }
1081}