1#[macro_use]
2extern crate lazy_static;
3#[macro_use]
4extern crate serde_derive;
5
6pub mod openapi;
7pub mod postman;
8
9pub use anyhow::Result;
10use convert_case::{Case, Casing};
11#[cfg(target_arch = "wasm32")]
12use gloo_utils::format::JsValueSerdeExt;
13use indexmap::{IndexMap, IndexSet};
14use openapi::v3_0::{self as openapi3, ObjectOrReference, Parameter, SecurityRequirement};
15use postman::AuthType;
16use std::collections::BTreeMap;
17#[cfg(target_arch = "wasm32")]
18use wasm_bindgen::prelude::*;
19
20static VAR_REPLACE_CREDITS: usize = 20;
21
22lazy_static! {
23 static ref VARIABLE_RE: regex::Regex = regex::Regex::new(r"\{\{([^{}]*?)\}\}").unwrap();
24 static ref URI_TEMPLATE_VARIABLE_RE: regex::Regex =
25 regex::Regex::new(r"\{([^{}]*?)\}").unwrap();
26}
27
28#[derive(Default)]
29pub struct TranspileOptions {
30 pub format: TargetFormat,
31}
32
33pub fn from_path(filename: &str, options: TranspileOptions) -> Result<String> {
34 let collection = std::fs::read_to_string(filename)?;
35 from_str(&collection, options)
36}
37
38#[cfg(not(target_arch = "wasm32"))]
39pub fn from_str(collection: &str, options: TranspileOptions) -> Result<String> {
40 let postman_spec: postman::Spec = serde_json::from_str(collection)?;
41 let oas_spec = Transpiler::transpile(postman_spec);
42 let oas_definition = match options.format {
43 TargetFormat::Json => openapi::to_json(&oas_spec),
44 TargetFormat::Yaml => openapi::to_yaml(&oas_spec),
45 }?;
46 Ok(oas_definition)
47}
48
49#[cfg(target_arch = "wasm32")]
50pub fn from_str(collection: &str, options: TranspileOptions) -> Result<String> {
51 let postman_spec: postman::Spec = serde_json::from_str(collection)?;
52 let oas_spec = Transpiler::transpile(postman_spec);
53 match options.format {
54 TargetFormat::Json => openapi::to_json(&oas_spec).map_err(|err| err.into()),
55 TargetFormat::Yaml => Err(anyhow::anyhow!(
56 "YAML is not supported for WebAssembly. Please convert from YAML to JSON."
57 )),
58 }
59}
60
61#[cfg(feature = "wee_alloc")]
64#[global_allocator]
65#[cfg(target_arch = "wasm32")]
66static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
67
68#[cfg(target_arch = "wasm32")]
69#[wasm_bindgen]
70pub fn transpile(collection: JsValue) -> std::result::Result<JsValue, JsValue> {
71 let postman_spec: std::result::Result<postman::Spec, serde_json::Error> =
72 collection.into_serde();
73 match postman_spec {
74 Ok(s) => {
75 let oas_spec = Transpiler::transpile(s);
76 let oas_definition = JsValue::from_serde(&oas_spec);
77 match oas_definition {
78 Ok(val) => Ok(val),
79 Err(err) => Err(JsValue::from_str(&err.to_string())),
80 }
81 }
82 Err(err) => Err(JsValue::from_str(&err.to_string())),
83 }
84}
85
86#[derive(PartialEq, Eq, Debug, Default)]
87pub enum TargetFormat {
88 Json,
89 #[default]
90 Yaml,
91}
92
93impl std::str::FromStr for TargetFormat {
94 type Err = &'static str;
95 fn from_str(s: &str) -> Result<Self, Self::Err> {
96 match s {
97 "json" => Ok(TargetFormat::Json),
98 "yaml" => Ok(TargetFormat::Yaml),
99 _ => Err("invalid format"),
100 }
101 }
102}
103
104pub struct Transpiler<'a> {
105 variable_map: &'a BTreeMap<String, serde_json::value::Value>,
106}
107
108struct TranspileState<'a> {
109 oas: &'a mut openapi3::Spec,
110 operation_ids: &'a mut BTreeMap<String, usize>,
111 auth_stack: &'a mut Vec<SecurityRequirement>,
112 hierarchy: &'a mut Vec<String>,
113}
114
115impl<'a> Transpiler<'a> {
116 pub fn new(variable_map: &'a BTreeMap<String, serde_json::value::Value>) -> Self {
117 Self { variable_map }
118 }
119
120 pub fn transpile(spec: postman::Spec) -> openapi::OpenApi {
121 let description = extract_description(&spec.info.description);
122
123 let mut oas = openapi3::Spec {
124 openapi: String::from("3.0.3"),
125 info: openapi3::Info {
126 license: None,
127 contact: Some(openapi3::Contact::default()),
128 description,
129 terms_of_service: None,
130 version: String::from("1.0.0"),
131 title: spec.info.name,
132 },
133 components: None,
134 external_docs: None,
135 paths: IndexMap::new(),
136 security: None,
137 servers: Some(Vec::<openapi3::Server>::new()),
138 tags: Some(IndexSet::<openapi3::Tag>::new()),
139 };
140
141 let mut variable_map = BTreeMap::<String, serde_json::value::Value>::new();
142 if let Some(var) = spec.variable {
143 for v in var {
144 if let Some(v_name) = v.key {
145 if let Some(v_val) = v.value {
146 if v_val != serde_json::Value::String("".to_string()) {
147 variable_map.insert(v_name, v_val);
148 }
149 }
150 }
151 }
152 };
153
154 let mut operation_ids = BTreeMap::<String, usize>::new();
155 let mut hierarchy = Vec::<String>::new();
156 let mut state = TranspileState {
157 oas: &mut oas,
158 operation_ids: &mut operation_ids,
159 hierarchy: &mut hierarchy,
160 auth_stack: &mut Vec::<SecurityRequirement>::new(),
161 };
162
163 let transpiler = Transpiler {
164 variable_map: &mut variable_map,
165 };
166
167 if let Some(auth) = spec.auth {
168 let security = transpiler.transform_security(&mut state, &auth);
169 if let Some(pair) = security {
170 if let Some((name, scopes)) = pair {
171 state.oas.security = Some(vec![SecurityRequirement {
172 requirement: Some(BTreeMap::from([(name, scopes)])),
173 }]);
174 } else {
175 state.oas.security = Some(vec![SecurityRequirement { requirement: None }]);
176 }
177 }
178 }
179
180 transpiler.transform(&mut state, &spec.item);
181
182 openapi::OpenApi::V3_0(Box::new(oas))
183 }
184
185 fn transform(&self, state: &mut TranspileState, items: &[postman::Items]) {
186 for item in items {
187 if let Some(i) = &item.item {
188 let name = match &item.name {
189 Some(n) => n,
190 None => "<folder>",
191 };
192 let description = extract_description(&item.description);
193
194 self.transform_folder(state, i, name, description, &item.auth);
195 } else {
196 let name = match &item.name {
197 Some(n) => n,
198 None => "<request>",
199 };
200 self.transform_request(state, item, name);
201 }
202 }
203 }
204
205 fn transform_folder(
206 &self,
207 state: &mut TranspileState,
208 items: &[postman::Items],
209 name: &str,
210 description: Option<String>,
211 auth: &Option<postman::Auth>,
212 ) {
213 let mut pushed_tag = false;
214 let mut pushed_auth = false;
215
216 if let Some(t) = &mut state.oas.tags {
217 let mut tag = openapi3::Tag {
218 name: name.to_string(),
219 description,
220 };
221
222 let mut i: usize = 0;
223 while t.contains(&tag) {
224 i += 1;
225 tag.name = format!("{tagName}{i}", tagName = tag.name);
226 }
227
228 let name = tag.name.clone();
229 t.insert(tag);
230
231 state.hierarchy.push(name);
232
233 pushed_tag = true;
234 };
235
236 if let Some(auth) = auth {
237 let security = self.transform_security(state, auth);
238 if let Some(pair) = security {
239 if let Some((name, scopes)) = pair {
240 state.auth_stack.push(SecurityRequirement {
241 requirement: Some(BTreeMap::from([(name, scopes)])),
242 });
243 } else {
244 state
245 .auth_stack
246 .push(SecurityRequirement { requirement: None });
247 }
248 pushed_auth = true;
249 }
250 }
251
252 self.transform(state, items);
253
254 if pushed_tag {
255 state.hierarchy.pop();
256 }
257
258 if pushed_auth {
259 state.auth_stack.pop();
260 }
261 }
262
263 fn transform_request(&self, state: &mut TranspileState, item: &postman::Items, name: &str) {
264 if let Some(postman::RequestUnion::RequestClass(request)) = &item.request {
265 if let Some(postman::Url::UrlClass(u)) = &request.url {
266 if let Some(postman::Host::StringArray(parts)) = &u.host {
267 self.transform_server(state, u, parts);
268 }
269
270 let root_path: Vec<postman::PathElement> = vec![];
271 let paths = match &u.path {
272 Some(postman::UrlPath::UnionArray(p)) => p,
273 _ => &root_path,
274 };
275
276 let security_requirement = if let Some(auth) = &request.auth {
277 let security = self.transform_security(state, auth);
278 if let Some(pair) = security {
279 if let Some((name, scopes)) = pair {
280 Some(vec![SecurityRequirement {
281 requirement: Some(BTreeMap::from([(name, scopes)])),
282 }])
283 } else {
284 Some(vec![SecurityRequirement { requirement: None }])
285 }
286 } else {
287 None
288 }
289 } else if !state.auth_stack.is_empty() {
290 Some(vec![state.auth_stack.last().unwrap().clone()])
291 } else {
292 None
293 };
294
295 self.transform_paths(state, item, request, name, u, paths, security_requirement)
296 }
297 }
298 }
299
300 fn transform_server(
301 &self,
302 state: &mut TranspileState,
303 url: &postman::UrlClass,
304 parts: &[String],
305 ) {
306 let host = parts.join(".");
307 let mut proto = "".to_string();
308 if let Some(protocol) = &url.protocol {
309 proto = format!("{protocol}://", protocol = protocol.clone());
310 }
311 if let Some(s) = &mut state.oas.servers {
312 let mut server_url = format!("{proto}{host}");
313 server_url = self.resolve_variables(&server_url, VAR_REPLACE_CREDITS);
314 if !s.iter_mut().any(|srv| srv.url == server_url) {
315 let server = openapi3::Server {
316 url: server_url,
317 description: None,
318 variables: None,
319 };
320 s.push(server);
321 }
322 }
323 }
324
325 #[allow(clippy::too_many_arguments)]
326 fn transform_paths(
327 &self,
328 state: &mut TranspileState,
329 item: &postman::Items,
330 request: &postman::RequestClass,
331 request_name: &str,
332 url: &postman::UrlClass,
333 paths: &[postman::PathElement],
334 security_requirement: Option<Vec<SecurityRequirement>>,
335 ) {
336 let resolved_segments = paths
337 .iter()
338 .map(|segment| {
339 let mut seg = match segment {
340 postman::PathElement::PathClass(c) => c.clone().value.unwrap_or_default(),
341 postman::PathElement::String(c) => c.to_string(),
342 };
343 seg = self.resolve_variables_with_replace_fn(&seg, VAR_REPLACE_CREDITS, |s| {
344 VARIABLE_RE.replace_all(&s, "{$1}").to_string()
345 });
346 if !seg.is_empty() {
347 match &seg[0..1] {
348 ":" => format!("{{{}}}", &seg[1..]),
349 _ => seg.to_string(),
350 }
351 } else {
352 seg
353 }
354 })
355 .collect::<Vec<String>>();
356 let segments = "/".to_string() + &resolved_segments.join("/");
357
358 if !state.oas.paths.contains_key(&segments) {
363 state
364 .oas
365 .paths
366 .insert(segments.clone(), openapi3::PathItem::default());
367 }
368
369 let path = state.oas.paths.get_mut(&segments).unwrap();
370 let method = match &request.method {
371 Some(m) => m.to_lowercase(),
372 None => "get".to_string(),
373 };
374 let op_ref = match method.as_str() {
375 "get" => &mut path.get,
376 "post" => &mut path.post,
377 "put" => &mut path.put,
378 "delete" => &mut path.delete,
379 "patch" => &mut path.patch,
380 "options" => &mut path.options,
381 "trace" => &mut path.trace,
382 _ => &mut path.get,
383 };
384 let is_merge = op_ref.is_some();
385 if op_ref.is_none() {
386 *op_ref = Some(openapi3::Operation::default());
387 }
388 let op = op_ref.as_mut().unwrap();
389
390 if let Some(security_requirement) = security_requirement {
391 if let Some(security) = &mut op.security {
392 for sr in security_requirement {
393 if !security.contains(&sr) {
394 security.push(sr);
395 }
396 }
397 } else {
398 op.security = Some(security_requirement);
399 }
400 }
401
402 path.parameters = self.generate_path_parameters(&resolved_segments, &url.variable);
403
404 if !is_merge {
405 let mut op_id = request_name
406 .chars()
407 .map(|c| match c {
408 'A'..='Z' | 'a'..='z' | '0'..='9' => c,
409 _ => ' ',
410 })
411 .collect::<String>()
412 .from_case(Case::Title)
413 .to_case(Case::Camel);
414
415 match state.operation_ids.get_mut(&op_id) {
416 Some(v) => {
417 *v += 1;
418 op_id = format!("{op_id}{v}");
419 }
420 None => {
421 state.operation_ids.insert(op_id.clone(), 0);
422 }
423 }
424
425 op.operation_id = Some(op_id);
426 }
427
428 if let Some(qp) = &url.query {
429 if let Some(mut query_params) = self.generate_query_parameters(qp) {
430 match &op.parameters {
431 Some(params) => {
432 let mut cloned = params.clone();
433 for p1 in &mut query_params {
434 if let ObjectOrReference::Object(p1) = p1 {
435 let found = cloned.iter_mut().find(|p2| {
436 if let ObjectOrReference::Object(p2) = p2 {
437 p2.location == p1.location && p2.name == p1.name
438 } else {
439 false
440 }
441 });
442 if let Some(ObjectOrReference::Object(p2)) = found {
443 p2.schema = Some(Self::merge_schemas(
444 p2.schema.clone().unwrap(),
445 &p1.schema.clone().unwrap(),
446 ));
447 } else {
448 cloned.push(ObjectOrReference::Object(p1.clone()));
449 }
450 }
451 }
452 op.parameters = Some(cloned);
453 }
454 None => op.parameters = Some(query_params),
455 };
456 }
457 }
458
459 let mut content_type: Option<String> = None;
460
461 if let Some(postman::HeaderUnion::HeaderArray(headers)) = &request.header {
462 for header in headers
463 .iter()
464 .filter(|hdr| hdr.key.is_some() && hdr.value.is_some())
465 {
466 let key = header.key.as_ref().unwrap().to_lowercase();
467 let value = header.value.as_ref().unwrap();
468 if key == "accept" || key == "authorization" {
469 continue;
470 }
471 if key == "content-type" {
472 let content_type_parts: Vec<&str> = value.split(';').collect();
473 content_type = Some(content_type_parts[0].to_owned());
474 } else {
475 let param = Parameter {
476 location: "header".to_owned(),
477 name: key.to_owned(),
478 description: extract_description(&header.description),
479 schema: Some(openapi3::Schema {
480 schema_type: Some("string".to_owned()),
481 example: Some(serde_json::Value::String(value.to_owned())),
482 ..openapi3::Schema::default()
483 }),
484 ..Parameter::default()
485 };
486
487 if op.parameters.is_none() {
488 op.parameters = Some(vec![ObjectOrReference::Object(param)]);
489 } else {
490 let params = op.parameters.as_mut().unwrap();
491 let mut has_pushed = false;
492 for p in params {
493 if let ObjectOrReference::Object(p) = p {
494 if p.name == param.name && p.location == param.location {
495 if let Some(schema) = &p.schema {
496 has_pushed = true;
497 p.schema = Some(Self::merge_schemas(
498 schema.clone(),
499 ¶m.schema.clone().unwrap(),
500 ));
501 }
502 }
503 }
504 }
505 if !has_pushed {
506 op.parameters
507 .as_mut()
508 .unwrap()
509 .push(ObjectOrReference::Object(param));
510 }
511 }
512 }
513 }
514 }
515
516 if let Some(body) = &request.body {
517 self.extract_request_body(body, op, request_name, content_type);
518 }
519
520 if !is_merge {
521 let description = match extract_description(&request.description) {
522 Some(desc) => Some(desc),
523 None => Some(request_name.to_string()),
524 };
525
526 op.summary = Some(request_name.to_string());
527 op.description = description;
528 }
529
530 if !state.hierarchy.is_empty() {
531 op.tags = Some(state.hierarchy.clone());
532 }
533
534 if let Some(responses) = &item.response {
535 for r in responses.iter().flatten() {
536 if let Some(or) = &r.original_request {
537 if let Some(body) = &or.body {
538 content_type = Some("text/plain".to_string());
539 if let Some(options) = body.options.clone() {
540 if let Some(raw_options) = options.raw {
541 if raw_options.language.is_some() {
542 content_type = match raw_options.language.unwrap().as_str() {
543 "xml" => Some("application/xml".to_string()),
544 "json" => Some("application/json".to_string()),
545 "html" => Some("text/html".to_string()),
546 _ => Some("text/plain".to_string()),
547 }
548 }
549 }
550 }
551 self.extract_request_body(body, op, request_name, content_type);
552 }
553 }
554 let mut oas_response = openapi3::Response::default();
555 let mut response_media_types = BTreeMap::<String, openapi3::MediaType>::new();
556
557 if let Some(name) = &r.name {
558 oas_response.description = Some(name.clone());
559 }
560 if let Some(postman::Headers::UnionArray(headers)) = &r.header {
561 let mut oas_headers =
562 BTreeMap::<String, openapi3::ObjectOrReference<openapi3::Header>>::new();
563 for h in headers {
564 if let postman::HeaderElement::Header(hdr) = h {
565 if hdr.key.is_none()
566 || hdr.value.is_none()
567 || hdr.value.as_ref().unwrap().is_empty()
568 || hdr.key.as_ref().unwrap().to_lowercase() == "content-type"
569 {
570 continue;
571 }
572 let mut oas_header = openapi3::Header::default();
573 let header_schema = openapi3::Schema {
574 schema_type: Some("string".to_string()),
575 example: Some(serde_json::Value::String(
576 hdr.value.clone().unwrap().to_string(),
577 )),
578 ..Default::default()
579 };
580 oas_header.schema = Some(header_schema);
581
582 oas_headers.insert(
583 hdr.key.clone().unwrap(),
584 openapi3::ObjectOrReference::Object(oas_header),
585 );
586 }
587 }
588 if !oas_headers.is_empty() {
589 oas_response.headers = Some(oas_headers);
590 }
591 }
592 let mut response_content = openapi3::MediaType::default();
593 if let Some(raw) = &r.body {
594 let mut response_content_type: Option<String> = None;
595 let resolved_body = self.resolve_variables(raw, VAR_REPLACE_CREDITS);
596 let example_val;
597
598 match serde_json::from_str(&resolved_body) {
599 Ok(v) => match v {
600 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
601 response_content_type = Some("application/json".to_string());
602 if let Some(schema) = Self::generate_schema(&v) {
603 response_content.schema =
604 Some(openapi3::ObjectOrReference::Object(schema));
605 }
606 example_val = v;
607 }
608 _ => {
609 example_val = serde_json::Value::String(resolved_body);
610 }
611 },
612 _ => {
613 response_content_type = Some("text/plain".to_string());
615 example_val = serde_json::Value::String(resolved_body);
616 }
617 }
618 let mut example_map =
619 BTreeMap::<String, openapi3::ObjectOrReference<openapi3::Example>>::new();
620
621 let ex = openapi3::Example {
622 summary: None,
623 description: None,
624 value: Some(example_val),
625 };
626
627 let example_name = match &r.name {
628 Some(n) => n.to_string(),
629 None => "".to_string(),
630 };
631
632 example_map.insert(example_name, openapi3::ObjectOrReference::Object(ex));
633 let example = openapi3::MediaTypeExample::Examples {
634 examples: example_map,
635 };
636
637 response_content.examples = Some(example);
638
639 if response_content_type.is_none() {
640 response_content_type = Some("application/octet-stream".to_string());
641 }
642
643 response_media_types
644 .insert(response_content_type.clone().unwrap(), response_content);
645 }
646 oas_response.content = Some(response_media_types);
647
648 if let Some(code) = &r.code {
649 if let Some(existing_response) = op.responses.get_mut(&code.to_string()) {
650 let new_response = oas_response.clone();
651 if let Some(name) = &new_response.description {
652 existing_response.description = Some(
653 existing_response
654 .description
655 .clone()
656 .unwrap_or("".to_string())
657 + " / "
658 + name,
659 );
660 }
661
662 if let Some(headers) = new_response.headers {
663 let mut cloned_headers = headers.clone();
664 for (key, val) in headers {
665 cloned_headers.insert(key, val);
666 }
667 existing_response.headers = Some(cloned_headers);
668 }
669
670 let mut existing_content =
671 existing_response.content.clone().unwrap_or_default();
672 for (media_type, new_content) in new_response.content.unwrap() {
673 if let Some(existing_response_content) =
674 existing_content.get_mut(&media_type)
675 {
676 if let Some(openapi3::ObjectOrReference::Object(existing_schema)) =
677 existing_response_content.schema.clone()
678 {
679 if let Some(openapi3::ObjectOrReference::Object(new_schema)) =
680 new_content.schema
681 {
682 existing_response_content.schema =
683 Some(openapi3::ObjectOrReference::Object(
684 Self::merge_schemas(existing_schema, &new_schema),
685 ))
686 }
687 }
688
689 if let Some(openapi3::MediaTypeExample::Examples {
690 examples: existing_examples,
691 }) = &mut existing_response_content.examples
692 {
693 let new_example_map = match new_content.examples.unwrap() {
694 openapi3::MediaTypeExample::Examples { examples } => {
695 examples.clone()
696 }
697 _ => BTreeMap::<String, _>::new(),
698 };
699 for (key, value) in new_example_map.iter() {
700 existing_examples.insert(key.clone(), value.clone());
701 }
702 }
703 }
704 }
705 existing_response.content = Some(existing_content.clone());
706 } else {
707 op.responses.insert(code.to_string(), oas_response);
708 }
709 }
710 }
711 }
712
713 if !op.responses.contains_key("200")
714 && !op.responses.contains_key("201")
715 && !op.responses.contains_key("202")
716 && !op.responses.contains_key("203")
717 && !op.responses.contains_key("204")
718 && !op.responses.contains_key("205")
719 && !op.responses.contains_key("206")
720 && !op.responses.contains_key("207")
721 && !op.responses.contains_key("208")
722 && !op.responses.contains_key("226")
723 {
724 op.responses.insert(
725 "200".to_string(),
726 openapi3::Response {
727 description: Some("".to_string()),
728 ..openapi3::Response::default()
729 },
730 );
731 }
732 }
733
734 fn transform_security(
735 &self,
736 state: &mut TranspileState,
737 auth: &postman::Auth,
738 ) -> Option<Option<(String, Vec<String>)>> {
739 if state.oas.components.is_none() {
740 state.oas.components = Some(openapi3::Components::default());
741 }
742 if state
743 .oas
744 .components
745 .as_ref()
746 .unwrap()
747 .security_schemes
748 .is_none()
749 {
750 state.oas.components.as_mut().unwrap().security_schemes = Some(BTreeMap::new());
751 }
752 let security_schemes = state
753 .oas
754 .components
755 .as_mut()
756 .unwrap()
757 .security_schemes
758 .as_mut()
759 .unwrap();
760 let security = match auth.auth_type {
761 AuthType::Noauth => Some(None),
762 AuthType::Basic => {
763 let scheme = openapi3::SecurityScheme::Http {
764 scheme: "basic".to_string(),
765 bearer_format: None,
766 };
767 let name = "basicAuth".to_string();
768 security_schemes.insert(name.clone(), ObjectOrReference::Object(scheme));
769 Some(Some((name, vec![])))
770 }
771 AuthType::Digest => {
772 let scheme = openapi3::SecurityScheme::Http {
773 scheme: "digest".to_string(),
774 bearer_format: None,
775 };
776 let name = "digestAuth".to_string();
777 security_schemes.insert(name.clone(), ObjectOrReference::Object(scheme));
778 Some(Some((name, vec![])))
779 }
780 AuthType::Bearer => {
781 let scheme = openapi3::SecurityScheme::Http {
782 scheme: "bearer".to_string(),
783 bearer_format: None,
784 };
785 let name = "bearerAuth".to_string();
786 security_schemes.insert(name.clone(), ObjectOrReference::Object(scheme));
787 Some(Some((name, vec![])))
788 }
789 AuthType::Jwt => {
790 let scheme = openapi3::SecurityScheme::Http {
791 scheme: "bearer".to_string(),
792 bearer_format: Some("jwt".to_string()),
793 };
794 let name = "jwtBearerAuth".to_string();
795 security_schemes.insert(name.clone(), ObjectOrReference::Object(scheme));
796 Some(Some((name, vec![])))
797 }
798 AuthType::Apikey => {
799 let name = "apiKey".to_string();
800 if let Some(apikey) = &auth.apikey {
801 let scheme = openapi3::SecurityScheme::ApiKey {
802 name: self.resolve_variables(
803 apikey.key.as_ref().unwrap_or(&"Authorization".to_string()),
804 VAR_REPLACE_CREDITS,
805 ),
806 location: match apikey.location {
807 postman::ApiKeyLocation::Header => "header".to_string(),
808 postman::ApiKeyLocation::Query => "query".to_string(),
809 },
810 };
811 security_schemes.insert(name.clone(), ObjectOrReference::Object(scheme));
812 } else {
813 let scheme = openapi3::SecurityScheme::ApiKey {
814 name: "Authorization".to_string(),
815 location: "header".to_string(),
816 };
817 security_schemes.insert(name.clone(), ObjectOrReference::Object(scheme));
818 }
819 Some(Some((name, vec![])))
820 }
821 AuthType::Oauth2 => {
822 let name = "oauth2".to_string();
823 if let Some(oauth2) = &auth.oauth2 {
824 let mut flows: openapi3::Flows = Default::default();
825 let scopes = BTreeMap::from_iter(
826 oauth2
827 .scope
828 .clone()
829 .unwrap_or_default()
830 .iter()
831 .map(|s| self.resolve_variables(s, VAR_REPLACE_CREDITS))
832 .map(|s| (s.to_string(), s.to_string())),
833 );
834 let authorization_url = self.resolve_variables(
835 oauth2.auth_url.as_ref().unwrap_or(&"".to_string()),
836 VAR_REPLACE_CREDITS,
837 );
838 let token_url = self.resolve_variables(
839 oauth2.access_token_url.as_ref().unwrap_or(&"".to_string()),
840 VAR_REPLACE_CREDITS,
841 );
842 let refresh_url = oauth2
843 .refresh_token_url
844 .as_ref()
845 .map(|url| self.resolve_variables(url, VAR_REPLACE_CREDITS));
846 match oauth2.grant_type {
847 postman::Oauth2GrantType::AuthorizationCode
848 | postman::Oauth2GrantType::AuthorizationCodeWithPkce => {
849 flows.authorization_code = Some(openapi3::AuthorizationCodeFlow {
850 authorization_url,
851 token_url,
852 refresh_url,
853 scopes,
854 });
855 }
856 postman::Oauth2GrantType::ClientCredentials => {
857 flows.client_credentials = Some(openapi3::ClientCredentialsFlow {
858 token_url,
859 refresh_url,
860 scopes,
861 });
862 }
863 postman::Oauth2GrantType::PasswordCredentials => {
864 flows.password = Some(openapi3::PasswordFlow {
865 token_url,
866 refresh_url,
867 scopes,
868 });
869 }
870 postman::Oauth2GrantType::Implicit => {
871 flows.implicit = Some(openapi3::ImplicitFlow {
872 authorization_url,
873 refresh_url,
874 scopes,
875 });
876 }
877 }
878 let scheme = openapi3::SecurityScheme::OAuth2 {
879 flows: Box::new(flows),
880 };
881 security_schemes.insert(name.clone(), ObjectOrReference::Object(scheme));
882 Some(Some((name, oauth2.scope.clone().unwrap_or_default())))
883 } else {
884 let scheme = openapi3::SecurityScheme::OAuth2 {
885 flows: Default::default(),
886 };
887 security_schemes.insert(name.clone(), ObjectOrReference::Object(scheme));
888 Some(Some((name, vec![])))
889 }
890 }
891 _ => None,
892 };
893
894 security
895 }
896
897 fn extract_request_body(
898 &self,
899 body: &postman::Body,
900 op: &mut openapi3::Operation,
901 name: &str,
902 ct: Option<String>,
903 ) {
904 let mut content_type = ct;
905 let mut request_body = if let Some(ObjectOrReference::Object(rb)) = op.request_body.as_mut()
906 {
907 rb.clone()
908 } else {
909 openapi3::RequestBody::default()
910 };
911
912 let default_media_type = openapi3::MediaType::default();
913
914 if let Some(mode) = &body.mode {
915 match mode {
916 postman::Mode::Raw => {
917 content_type = Some("application/octet-stream".to_string());
918 if let Some(raw) = &body.raw {
919 let resolved_body = self.resolve_variables(raw, VAR_REPLACE_CREDITS);
920 let example_val;
921
922 match serde_json::from_str(&resolved_body) {
924 Ok(v) => match v {
925 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
926 content_type = Some("application/json".to_string());
927 let content = {
928 let ct = content_type.as_ref().unwrap();
929 if !request_body.content.contains_key(ct) {
930 request_body
931 .content
932 .insert(ct.clone(), default_media_type.clone());
933 }
934
935 request_body.content.get_mut(ct).unwrap()
936 };
937
938 if let Some(schema) = Self::generate_schema(&v) {
939 content.schema =
940 Some(openapi3::ObjectOrReference::Object(schema));
941 }
942 example_val = v;
943 }
944 _ => {
945 example_val = serde_json::Value::String(resolved_body);
946 }
947 },
948 _ => {
949 content_type = Some("text/plain".to_string());
950 if let Some(options) = body.options.clone() {
951 if let Some(raw_options) = options.raw {
952 if raw_options.language.is_some() {
953 content_type =
954 match raw_options.language.unwrap().as_str() {
955 "xml" => Some("application/xml".to_string()),
956 "json" => Some("application/json".to_string()),
957 "html" => Some("text/html".to_string()),
958 _ => Some("text/plain".to_string()),
959 }
960 }
961 }
962 }
963 example_val = serde_json::Value::String(resolved_body);
964 }
965 }
966
967 let content = {
968 let ct = content_type.as_ref().unwrap();
969 if !request_body.content.contains_key(ct) {
970 request_body
971 .content
972 .insert(ct.clone(), default_media_type.clone());
973 }
974
975 request_body.content.get_mut(ct).unwrap()
976 };
977
978 let examples = content.examples.clone().unwrap_or(
979 openapi3::MediaTypeExample::Examples {
980 examples: BTreeMap::new(),
981 },
982 );
983
984 let example = openapi3::Example {
985 summary: None,
986 description: None,
987 value: Some(example_val),
988 };
989
990 if let openapi3::MediaTypeExample::Examples { examples: mut ex } = examples
991 {
992 ex.insert(name.to_string(), ObjectOrReference::Object(example));
993 content.examples =
994 Some(openapi3::MediaTypeExample::Examples { examples: ex });
995 }
996 *content = content.clone();
997 }
998 }
999 postman::Mode::Urlencoded => {
1000 content_type = Some("application/x-www-form-urlencoded".to_string());
1001 let content = {
1002 let ct = content_type.as_ref().unwrap();
1003 if !request_body.content.contains_key(ct) {
1004 request_body
1005 .content
1006 .insert(ct.clone(), default_media_type.clone());
1007 }
1008
1009 request_body.content.get_mut(ct).unwrap()
1010 };
1011 if let Some(urlencoded) = &body.urlencoded {
1012 let mut oas_data = serde_json::Map::new();
1013 for i in urlencoded {
1014 if let Some(v) = &i.value {
1015 let value = serde_json::Value::String(v.to_string());
1016 oas_data.insert(i.key.clone(), value);
1017 }
1018 }
1019 let oas_obj = serde_json::Value::Object(oas_data);
1020 if let Some(schema) = Self::generate_schema(&oas_obj) {
1021 content.schema = Some(openapi3::ObjectOrReference::Object(schema));
1022 }
1023
1024 let examples = content.examples.clone().unwrap_or(
1025 openapi3::MediaTypeExample::Examples {
1026 examples: BTreeMap::new(),
1027 },
1028 );
1029
1030 let example = openapi3::Example {
1031 summary: None,
1032 description: None,
1033 value: Some(oas_obj),
1034 };
1035
1036 if let openapi3::MediaTypeExample::Examples { examples: mut ex } = examples
1037 {
1038 ex.insert(name.to_string(), ObjectOrReference::Object(example));
1039 content.examples =
1040 Some(openapi3::MediaTypeExample::Examples { examples: ex });
1041 }
1042 }
1043 }
1044 postman::Mode::Formdata => {
1045 content_type = Some("multipart/form-data".to_string());
1046 let content = {
1047 let ct = content_type.as_ref().unwrap();
1048 if !request_body.content.contains_key(ct) {
1049 request_body
1050 .content
1051 .insert(ct.clone(), default_media_type.clone());
1052 }
1053
1054 request_body.content.get_mut(ct).unwrap()
1055 };
1056
1057 let mut schema = openapi3::Schema {
1058 schema_type: Some("object".to_string()),
1059 ..Default::default()
1060 };
1061 let mut properties = BTreeMap::<String, openapi3::Schema>::new();
1062
1063 if let Some(formdata) = &body.formdata {
1064 for i in formdata {
1065 if let Some(t) = &i.form_parameter_type {
1066 let is_binary = t.as_str() == "file";
1067 if let Some(v) = &i.value {
1068 let value = serde_json::Value::String(v.to_string());
1069 let prop_schema = Self::generate_schema(&value);
1070 if let Some(mut prop_schema) = prop_schema {
1071 if is_binary {
1072 prop_schema.format = Some("binary".to_string());
1073 }
1074 prop_schema.description =
1075 extract_description(&i.description);
1076 properties.insert(i.key.clone(), prop_schema);
1077 }
1078 } else {
1079 let mut prop_schema = openapi3::Schema {
1080 schema_type: Some("string".to_string()),
1081 description: extract_description(&i.description),
1082 ..Default::default()
1083 };
1084 if is_binary {
1085 prop_schema.format = Some("binary".to_string());
1086 }
1087 properties.insert(i.key.clone(), prop_schema);
1088 }
1089 }
1090 }
1092 schema.properties = Some(properties);
1093 content.schema = Some(openapi3::ObjectOrReference::Object(schema));
1094 }
1095 }
1096
1097 postman::Mode::GraphQl => {
1098 content_type = Some("application/json".to_string());
1099 let content = {
1100 let ct = content_type.as_ref().unwrap();
1101 if !request_body.content.contains_key(ct) {
1102 request_body
1103 .content
1104 .insert(ct.clone(), default_media_type.clone());
1105 }
1106
1107 request_body.content.get_mut(ct).unwrap()
1108 };
1109
1110 content.schema = Some(ObjectOrReference::Object(openapi3::Schema {
1112 schema_type: Some("object".to_owned()),
1113 properties: Some(BTreeMap::from([
1114 (
1115 "query".to_owned(),
1116 openapi3::Schema {
1117 schema_type: Some("string".to_owned()),
1118 ..openapi3::Schema::default()
1119 },
1120 ),
1121 (
1122 "variables".to_owned(),
1123 openapi3::Schema {
1124 schema_type: Some("object".to_owned()),
1125 ..openapi3::Schema::default()
1126 },
1127 ),
1128 ])),
1129 ..openapi3::Schema::default()
1130 }));
1131
1132 if let Some(postman::GraphQlBody::GraphQlBodyClass(graphql)) = &body.graphql {
1133 if let Some(query) = &graphql.query {
1134 let mut example_map = serde_json::Map::new();
1135 example_map.insert("query".to_owned(), query.to_owned().into());
1136 if let Some(vars) = &graphql.variables {
1137 if let Ok(vars) = serde_json::from_str::<serde_json::Value>(vars) {
1138 example_map.insert("variables".to_owned(), vars);
1139 }
1140 }
1141
1142 let example = openapi3::MediaTypeExample::Example {
1143 example: serde_json::Value::Object(example_map),
1144 };
1145 content.examples = Some(example);
1146 }
1147 }
1148 }
1149 _ => content_type = Some("application/octet-stream".to_string()),
1150 }
1151 }
1152
1153 if content_type.is_none() {
1154 content_type = Some("application/octet-stream".to_string());
1155 request_body
1156 .content
1157 .insert(content_type.unwrap(), default_media_type);
1158 }
1159
1160 op.request_body = Some(openapi3::ObjectOrReference::Object(request_body));
1161 }
1162
1163 fn resolve_variables(&self, segment: &str, sub_replace_credits: usize) -> String {
1164 self.resolve_variables_with_replace_fn(segment, sub_replace_credits, |s| s)
1165 }
1166
1167 fn resolve_variables_with_replace_fn(
1168 &self,
1169 segment: &str,
1170 sub_replace_credits: usize,
1171 replace_fn: fn(String) -> String,
1172 ) -> String {
1173 let s = segment.to_string();
1174
1175 if sub_replace_credits == 0 {
1176 return s;
1177 }
1178
1179 if let Some(cap) = VARIABLE_RE.captures(&s) {
1180 if cap.len() > 1 {
1181 for n in 1..cap.len() {
1182 let capture = &cap[n].to_string();
1183 if let Some(v) = self.variable_map.get(capture) {
1184 if let Some(v2) = v.as_str() {
1185 let re = regex::Regex::new(®ex::escape(&cap[0])).unwrap();
1186 return self.resolve_variables(
1187 &re.replace_all(&s, v2),
1188 sub_replace_credits - 1,
1189 );
1190 }
1191 }
1192 }
1193 }
1194 }
1195
1196 replace_fn(s)
1197 }
1198
1199 fn generate_schema(value: &serde_json::Value) -> Option<openapi3::Schema> {
1200 match value {
1201 serde_json::Value::Object(m) => {
1202 let mut schema = openapi3::Schema {
1203 schema_type: Some("object".to_string()),
1204 ..Default::default()
1205 };
1206
1207 let mut properties = BTreeMap::<String, openapi3::Schema>::new();
1208
1209 for (key, val) in m.iter() {
1210 if let Some(v) = Self::generate_schema(val) {
1211 properties.insert(key.to_string(), v);
1212 }
1213 }
1214
1215 schema.properties = Some(properties);
1216 Some(schema)
1217 }
1218 serde_json::Value::Array(a) => {
1219 let mut schema = openapi3::Schema {
1220 schema_type: Some("array".to_string()),
1221 ..Default::default()
1222 };
1223
1224 let mut item_schema = openapi3::Schema::default();
1225
1226 for n in 0..a.len() {
1227 if let Some(i) = a.get(n) {
1228 if let Some(i) = Self::generate_schema(i) {
1229 if n == 0 {
1230 item_schema = i;
1231 } else {
1232 item_schema = Self::merge_schemas(item_schema, &i);
1233 }
1234 }
1235 }
1236 }
1237
1238 schema.items = Some(Box::new(item_schema));
1239 schema.example = Some(value.clone());
1240
1241 Some(schema)
1242 }
1243 serde_json::Value::String(_) => {
1244 let schema = openapi3::Schema {
1245 schema_type: Some("string".to_string()),
1246 example: Some(value.clone()),
1247 ..Default::default()
1248 };
1249 Some(schema)
1250 }
1251 serde_json::Value::Number(_) => {
1252 let schema = openapi3::Schema {
1253 schema_type: Some("number".to_string()),
1254 example: Some(value.clone()),
1255 ..Default::default()
1256 };
1257 Some(schema)
1258 }
1259 serde_json::Value::Bool(_) => {
1260 let schema = openapi3::Schema {
1261 schema_type: Some("boolean".to_string()),
1262 example: Some(value.clone()),
1263 ..Default::default()
1264 };
1265 Some(schema)
1266 }
1267 serde_json::Value::Null => {
1268 let schema = openapi3::Schema {
1269 nullable: Some(true),
1270 example: Some(value.clone()),
1271 ..Default::default()
1272 };
1273 Some(schema)
1274 }
1275 }
1276 }
1277
1278 fn merge_schemas(mut original: openapi3::Schema, new: &openapi3::Schema) -> openapi3::Schema {
1279 if original.nullable.is_none() && new.nullable.is_some() {
1282 original.nullable = new.nullable;
1283 }
1284
1285 if let Some(original_nullable) = original.nullable {
1288 if let Some(new_nullable) = new.nullable {
1289 if new_nullable != original_nullable {
1290 original.nullable = Some(true);
1291 }
1292 }
1293 }
1294
1295 if let Some(ref mut any_of) = original.any_of {
1296 any_of.push(openapi3::ObjectOrReference::Object(new.clone()));
1297 return original;
1298 }
1299
1300 if original.schema_type.is_none() && new.schema_type.is_some() && new.any_of.is_none() {
1302 original.schema_type = new.schema_type.clone();
1303 }
1304
1305 if let Some(t) = &original.schema_type {
1307 if let "object" = t.as_str() {
1308 if let Some(original_properties) = &mut original.properties {
1309 if let Some(new_properties) = &new.properties {
1310 for (key, val) in original_properties.iter_mut() {
1311 if let Some(v) = new_properties.get(key) {
1312 let prop = v;
1313 *val = Self::merge_schemas(val.clone(), prop);
1314 }
1315 }
1316
1317 for (key, val) in new_properties.iter() {
1318 if !original_properties.contains_key(key) {
1319 original_properties.insert(key.to_string(), val.clone());
1320 }
1321 }
1322 }
1323 }
1324 }
1325 }
1326
1327 if let Some(ref original_type) = original.schema_type {
1328 if let Some(ref new_type) = new.schema_type {
1329 if new_type != original_type {
1330 let cloned = original.clone();
1331 original.schema_type = None;
1332 original.properties = None;
1333 original.items = None;
1334 original.any_of = Some(vec![
1335 openapi3::ObjectOrReference::Object(cloned),
1336 openapi3::ObjectOrReference::Object(new.clone()),
1337 ]);
1338 }
1339 }
1340 }
1341
1342 original
1343 }
1344
1345 fn generate_path_parameters(
1346 &self,
1347 resolved_segments: &[String],
1348 postman_variables: &Option<Vec<postman::Variable>>,
1349 ) -> Option<Vec<openapi3::ObjectOrReference<openapi3::Parameter>>> {
1350 let params: Vec<openapi3::ObjectOrReference<openapi3::Parameter>> = resolved_segments
1351 .iter()
1352 .flat_map(|segment| {
1353 URI_TEMPLATE_VARIABLE_RE
1354 .captures_iter(segment.as_str())
1355 .map(|capture| {
1356 let var = capture.get(1).unwrap().as_str();
1357 let mut param = Parameter {
1358 name: var.to_owned(),
1359 location: "path".to_owned(),
1360 required: Some(true),
1361 ..Parameter::default()
1362 };
1363
1364 let mut schema = openapi3::Schema {
1365 schema_type: Some("string".to_string()),
1366 ..Default::default()
1367 };
1368 if let Some(path_val) = &postman_variables {
1369 if let Some(p) = path_val.iter().find(|p| match &p.key {
1370 Some(k) => k == var,
1371 _ => false,
1372 }) {
1373 param.description = extract_description(&p.description);
1374 if let Some(pval) = &p.value {
1375 if let Some(pval_val) = pval.as_str() {
1376 schema.example = Some(serde_json::Value::String(
1377 self.resolve_variables(pval_val, VAR_REPLACE_CREDITS),
1378 ));
1379 }
1380 }
1381 }
1382 }
1383 param.schema = Some(schema);
1384 openapi3::ObjectOrReference::Object(param)
1385 })
1386 })
1387 .collect();
1388
1389 if !params.is_empty() {
1390 Some(params)
1391 } else {
1392 None
1393 }
1394 }
1395
1396 fn generate_query_parameters(
1397 &self,
1398 query_params: &[postman::QueryParam],
1399 ) -> Option<Vec<openapi3::ObjectOrReference<openapi3::Parameter>>> {
1400 let mut keys = vec![];
1401 let params = query_params
1402 .iter()
1403 .filter_map(|qp| match qp.key {
1404 Some(ref key) => {
1405 if keys.contains(&key.as_str()) {
1406 return None;
1407 }
1408
1409 keys.push(key);
1410 let param = Parameter {
1411 name: key.to_owned(),
1412 description: extract_description(&qp.description),
1413 location: "query".to_owned(),
1414 schema: Some(openapi3::Schema {
1415 schema_type: Some("string".to_string()),
1416 example: qp.value.as_ref().map(|pval| {
1417 serde_json::Value::String(
1418 self.resolve_variables(pval, VAR_REPLACE_CREDITS),
1419 )
1420 }),
1421 ..openapi3::Schema::default()
1422 }),
1423 ..Parameter::default()
1424 };
1425
1426 Some(openapi3::ObjectOrReference::Object(param))
1427 }
1428 None => None,
1429 })
1430 .collect::<Vec<openapi3::ObjectOrReference<openapi3::Parameter>>>();
1431
1432 if !params.is_empty() {
1433 Some(params)
1434 } else {
1435 None
1436 }
1437 }
1438}
1439
1440fn extract_description(description: &Option<postman::DescriptionUnion>) -> Option<String> {
1441 match description {
1442 Some(d) => match d {
1443 postman::DescriptionUnion::String(s) => Some(s.to_string()),
1444 postman::DescriptionUnion::Description(desc) => {
1445 desc.content.as_ref().map(|c| c.to_string())
1446 }
1447 },
1448 None => None,
1449 }
1450}
1451
1452#[cfg(not(target_arch = "wasm32"))]
1453#[cfg(test)]
1454mod tests {
1455 use super::*;
1456 use openapi::v3_0::{MediaTypeExample, ObjectOrReference, Parameter, Schema};
1457 use openapi::OpenApi;
1458 use postman::Spec;
1459
1460 #[test]
1461 fn test_extract_description() {
1462 let description = Some(postman::DescriptionUnion::String("test".to_string()));
1463 assert_eq!(extract_description(&description), Some("test".to_string()));
1464
1465 let description = Some(postman::DescriptionUnion::Description(
1466 postman::Description {
1467 content: Some("test".to_string()),
1468 ..postman::Description::default()
1469 },
1470 ));
1471 assert_eq!(extract_description(&description), Some("test".to_string()));
1472
1473 let description = None;
1474 assert_eq!(extract_description(&description), None);
1475 }
1476
1477 #[test]
1478 fn test_generate_path_parameters() {
1479 let empty_map = BTreeMap::<_, _>::new();
1480 let transpiler = Transpiler::new(&empty_map);
1481 let postman_variables = Some(vec![postman::Variable {
1482 key: Some("test".to_string()),
1483 value: Some(serde_json::Value::String("test_value".to_string())),
1484 description: None,
1485 ..postman::Variable::default()
1486 }]);
1487 let path_params = ["/test/".to_string(), "{{test_value}}".to_string()];
1488 let params = transpiler.generate_path_parameters(&path_params, &postman_variables);
1489 assert_eq!(params.unwrap().len(), 1);
1490 }
1491
1492 #[test]
1493 fn test_generate_query_parameters() {
1494 let empty_map = BTreeMap::<_, _>::new();
1495 let transpiler = Transpiler::new(&empty_map);
1496 let query_params = vec![postman::QueryParam {
1497 key: Some("test".to_string()),
1498 value: Some("{{test}}".to_string()),
1499 description: None,
1500 ..postman::QueryParam::default()
1501 }];
1502 let params = transpiler.generate_query_parameters(&query_params);
1503 assert_eq!(params.unwrap().len(), 1);
1504 }
1505
1506 #[test]
1507 fn it_preserves_order_on_paths() {
1508 let spec: Spec = serde_json::from_str(get_fixture("echo.postman.json").as_ref()).unwrap();
1509 let oas = Transpiler::transpile(spec);
1510 let ordered_paths = [
1511 "/get",
1512 "/post",
1513 "/put",
1514 "/patch",
1515 "/delete",
1516 "/headers",
1517 "/response-headers",
1518 "/basic-auth",
1519 "/digest-auth",
1520 "/auth/hawk",
1521 "/oauth1",
1522 "/cookies/set",
1523 "/cookies",
1524 "/cookies/delete",
1525 "/status/200",
1526 "/stream/5",
1527 "/delay/2",
1528 "/encoding/utf8",
1529 "/gzip",
1530 "/deflate",
1531 "/ip",
1532 "/time/now",
1533 "/time/valid",
1534 "/time/format",
1535 "/time/unit",
1536 "/time/add",
1537 "/time/subtract",
1538 "/time/start",
1539 "/time/object",
1540 "/time/before",
1541 "/time/after",
1542 "/time/between",
1543 "/time/leap",
1544 "/transform/collection",
1545 "/{method}/hello",
1546 ];
1547 let OpenApi::V3_0(s) = oas;
1548 let keys = s.paths.keys().enumerate();
1549 for (i, k) in keys {
1550 assert_eq!(k, ordered_paths[i])
1551 }
1552 }
1553
1554 #[test]
1555 fn it_uses_the_correct_content_type_for_form_urlencoded_data() {
1556 let spec: Spec = serde_json::from_str(get_fixture("echo.postman.json").as_ref()).unwrap();
1557 let oas = Transpiler::transpile(spec);
1558 match oas {
1559 OpenApi::V3_0(oas) => {
1560 let b = oas
1561 .paths
1562 .get("/post")
1563 .unwrap()
1564 .post
1565 .as_ref()
1566 .unwrap()
1567 .request_body
1568 .as_ref()
1569 .unwrap();
1570 if let ObjectOrReference::Object(b) = b {
1571 assert!(b.content.contains_key("application/x-www-form-urlencoded"));
1572 }
1573 }
1574 }
1575 }
1576
1577 #[test]
1578 fn it_generates_headers_from_the_request() {
1579 let spec: Spec = serde_json::from_str(get_fixture("echo.postman.json").as_ref()).unwrap();
1580 let oas = Transpiler::transpile(spec);
1581 match oas {
1582 OpenApi::V3_0(oas) => {
1583 let params = oas
1584 .paths
1585 .get("/headers")
1586 .unwrap()
1587 .get
1588 .as_ref()
1589 .unwrap()
1590 .parameters
1591 .as_ref()
1592 .unwrap();
1593 let header = params
1594 .iter()
1595 .find(|p| {
1596 if let ObjectOrReference::Object(p) = p {
1597 p.location == "header"
1598 } else {
1599 false
1600 }
1601 })
1602 .unwrap();
1603 let expected = ObjectOrReference::Object(Parameter {
1604 name: "my-sample-header".to_owned(),
1605 location: "header".to_owned(),
1606 description: Some("My Sample Header".to_owned()),
1607 schema: Some(Schema {
1608 schema_type: Some("string".to_owned()),
1609 example: Some(serde_json::Value::String(
1610 "Lorem ipsum dolor sit amet".to_owned(),
1611 )),
1612 ..Schema::default()
1613 }),
1614 ..Parameter::default()
1615 });
1616 assert_eq!(header, &expected);
1617 }
1618 }
1619 }
1620
1621 #[test]
1622 fn it_generates_root_path_when_no_path_exists_in_collection() {
1623 let spec: Spec =
1624 serde_json::from_str(get_fixture("only-root-path.postman.json").as_ref()).unwrap();
1625 let oas = Transpiler::transpile(spec);
1626 match oas {
1627 OpenApi::V3_0(oas) => {
1628 assert!(oas.paths.contains_key("/"));
1629 }
1630 }
1631 }
1632
1633 #[test]
1634 fn it_parses_graphql_request_bodies() {
1635 let spec: Spec =
1636 serde_json::from_str(get_fixture("graphql.postman.json").as_ref()).unwrap();
1637 let oas = Transpiler::transpile(spec);
1638 match oas {
1639 OpenApi::V3_0(oas) => {
1640 let body = oas
1641 .paths
1642 .get("/")
1643 .unwrap()
1644 .post
1645 .as_ref()
1646 .unwrap()
1647 .request_body
1648 .as_ref()
1649 .unwrap();
1650
1651 if let ObjectOrReference::Object(body) = body {
1652 assert!(body.content.contains_key("application/json"));
1653 let content = body.content.get("application/json").unwrap();
1654 let schema = content.schema.as_ref().unwrap();
1655 if let ObjectOrReference::Object(schema) = schema {
1656 let props = schema.properties.as_ref().unwrap();
1657 assert!(props.contains_key("query"));
1658 assert!(props.contains_key("variables"));
1659 }
1660 let examples = content.examples.as_ref().unwrap();
1661 if let MediaTypeExample::Example { example } = examples {
1662 let example: serde_json::Map<String, serde_json::Value> =
1663 serde_json::from_value(example.clone()).unwrap();
1664 assert!(example.contains_key("query"));
1665 assert!(example.contains_key("variables"));
1666 }
1667 }
1668 }
1669 }
1670 }
1671
1672 #[test]
1673 fn it_collapses_duplicate_query_params() {
1674 let spec: Spec =
1675 serde_json::from_str(get_fixture("duplicate-query-params.postman.json").as_ref())
1676 .unwrap();
1677 let oas = Transpiler::transpile(spec);
1678 match oas {
1679 OpenApi::V3_0(oas) => {
1680 let query_param_names = oas
1681 .paths
1682 .get("/v2/json-rpc/{site id}")
1683 .unwrap()
1684 .post
1685 .as_ref()
1686 .unwrap()
1687 .parameters
1688 .as_ref()
1689 .unwrap()
1690 .iter()
1691 .filter_map(|p| match p {
1692 ObjectOrReference::Object(p) => {
1693 if p.location == "query" {
1694 Some(p.name.clone())
1695 } else {
1696 None
1697 }
1698 }
1699 _ => None,
1700 })
1701 .collect::<Vec<String>>();
1702
1703 assert!(!query_param_names.is_empty());
1704
1705 let duplicates = (1..query_param_names.len())
1706 .filter_map(|i| {
1707 if query_param_names[i..].contains(&query_param_names[i - 1]) {
1708 Some(query_param_names[i - 1].clone())
1709 } else {
1710 None
1711 }
1712 })
1713 .collect::<std::collections::HashSet<String>>();
1714
1715 assert!(duplicates.is_empty(), "duplicates: {duplicates:?}");
1716 }
1717 }
1718 }
1719
1720 #[test]
1721 fn it_uses_the_security_requirement_on_operations() {
1722 let spec: Spec = serde_json::from_str(get_fixture("echo.postman.json").as_ref()).unwrap();
1723 let oas = Transpiler::transpile(spec);
1724 match oas {
1725 OpenApi::V3_0(oas) => {
1726 let sr1 = oas
1727 .paths
1728 .get("/basic-auth")
1729 .unwrap()
1730 .get
1731 .as_ref()
1732 .unwrap()
1733 .security
1734 .as_ref()
1735 .unwrap();
1736 assert_eq!(
1737 sr1.first()
1738 .unwrap()
1739 .requirement
1740 .as_ref()
1741 .unwrap()
1742 .get("basicAuth"),
1743 Some(&vec![])
1744 );
1745 let sr1 = oas
1746 .paths
1747 .get("/digest-auth")
1748 .unwrap()
1749 .get
1750 .as_ref()
1751 .unwrap()
1752 .security
1753 .as_ref()
1754 .unwrap();
1755 assert_eq!(
1756 sr1.first()
1757 .unwrap()
1758 .requirement
1759 .as_ref()
1760 .unwrap()
1761 .get("digestAuth"),
1762 Some(&vec![])
1763 );
1764
1765 let schemes = oas.components.unwrap().security_schemes.unwrap();
1766 let basic = schemes.get("basicAuth").unwrap();
1767 if let ObjectOrReference::Object(basic) = basic {
1768 match basic {
1769 openapi3::SecurityScheme::Http { scheme, .. } => {
1770 assert_eq!(scheme, "basic");
1771 }
1772 _ => panic!("Expected Http Security Scheme"),
1773 }
1774 }
1775 let digest = schemes.get("digestAuth").unwrap();
1776 if let ObjectOrReference::Object(digest) = digest {
1777 match digest {
1778 openapi3::SecurityScheme::Http { scheme, .. } => {
1779 assert_eq!(scheme, "digest");
1780 }
1781 _ => panic!("Expected Http Security Scheme"),
1782 }
1783 }
1784 }
1785 }
1786 }
1787
1788 fn get_fixture(filename: &str) -> String {
1789 use std::fs;
1790
1791 let filename: std::path::PathBuf =
1792 [env!("CARGO_MANIFEST_DIR"), "./tests/fixtures/", filename]
1793 .iter()
1794 .collect();
1795 let file = filename.into_os_string().into_string().unwrap();
1796 fs::read_to_string(file).unwrap()
1797 }
1798}