postman2openapi/
lib.rs

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// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
62// allocator.
63#[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        // TODO: Because of variables, we can actually get duplicate paths.
359        // - /admin/{subresource}/{subresourceId}
360        // - /admin/{subresource2}/{subresource2Id}
361        // Throw a warning?
362        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                                            &param.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                            // TODO: Check if XML, HTML, JavaScript
614                            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                        //set content type based on options or inference.
923                        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                            // NOTE: Postman doesn't store the content type of multipart files. :(
1091                        }
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                    // The schema is the same for every GraphQL request.
1111                    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(&regex::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 the new schema has a nullable Option but the original doesn't,
1280        // set the original nullable to the new one.
1281        if original.nullable.is_none() && new.nullable.is_some() {
1282            original.nullable = new.nullable;
1283        }
1284
1285        // If both original and new have a nullable Option,
1286        // If any of their values is true, set to true.
1287        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        // Reset the schema type.
1301        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 both types are objects, merge the schemas of each property.
1306        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}