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