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