openapiv3/v2/
upgrade.rs

1use super::schema as v2;
2use crate as v3;
3use crate::{Parameter, StatusCode};
4use indexmap::IndexMap;
5use std::convert::TryInto;
6
7trait TryRemove<T> {
8    fn try_remove(&mut self, i: usize) -> Option<T>;
9}
10
11impl<T> TryRemove<T> for Vec<T> {
12    fn try_remove(&mut self, i: usize) -> Option<T> {
13        self.get(i)?;
14        Some(self.remove(i))
15    }
16}
17
18impl Into<v3::OpenAPI> for v2::OpenAPI {
19    fn into(self) -> v3::OpenAPI {
20        let v2::OpenAPI {
21            swagger: _,
22            info,
23            host,
24            base_path,
25            schemes,
26            consumes: _,
27            produces: _,
28            paths,
29            definitions,
30            parameters,
31            responses,
32            security_definitions,
33            security,
34            tags,
35            external_docs,
36            extensions,
37        } = self;
38        let mut components = v3::Components::default();
39
40        components.schemas = definitions
41            .unwrap_or_default()
42            .into_iter()
43            .map(|(k, v)| (k, v3::RefOr::Item(v.into())))
44            .collect();
45
46        components.parameters = parameters
47            .unwrap_or_default()
48            .into_iter()
49            .filter_map(|(k, v)| {
50                let v: v3::RefOr<v3::Parameter> = v.try_into().ok()?;
51                Some((k, v))
52            })
53            .collect();
54
55        components.responses = responses
56            .unwrap_or_default()
57            .into_iter()
58            .map(|(k, v)| (k, v.into()))
59            .collect();
60
61        components.security_schemes = security_definitions
62            .unwrap_or_default()
63            .into_iter()
64            .map(|(k, v)| (k, v.into()))
65            .collect();
66
67        v3::OpenAPI {
68            openapi: "3.0.3".to_string(),
69            info: info.into(),
70            servers: host
71                .map(|h| {
72                    let scheme = schemes
73                        .and_then(|mut s| {
74                            if s.len() >= 1 {
75                                Some(s.remove(0))
76                            } else {
77                                None
78                            }
79                        })
80                        .map(|s| s.as_str())
81                        .unwrap_or("http");
82                    let url = format!("{}://{}{}", scheme, h, base_path.unwrap_or_default());
83                    vec![v3::Server {
84                        url,
85                        ..v3::Server::default()
86                    }]
87                })
88                .unwrap_or_default(),
89            paths: paths.into(),
90            components,
91            security: security.unwrap_or_default(),
92            tags: tags
93                .unwrap_or_default()
94                .into_iter()
95                .map(|t| t.into())
96                .collect(),
97            external_docs: external_docs
98                .and_then(|mut e| e.try_remove(0))
99                .map(|e| e.into()),
100            extensions,
101        }
102    }
103}
104
105impl Into<v3::Paths> for IndexMap<String, v2::PathItem> {
106    fn into(self) -> v3::Paths {
107        v3::Paths {
108            paths: self.into_iter().map(|(k, v)| (k, v.into())).collect(),
109            extensions: Default::default(),
110        }
111    }
112}
113
114impl Into<v3::RefOr<v3::PathItem>> for v2::PathItem {
115    fn into(self) -> v3::RefOr<v3::PathItem> {
116        let v2::PathItem {
117            get,
118            put,
119            post,
120            delete,
121            options,
122            head,
123            patch,
124            parameters,
125            extensions,
126        } = self;
127        v3::RefOr::Item(v3::PathItem {
128            summary: None,
129            description: None,
130            get: get.map(|op| op.into()),
131            put: put.map(|op| op.into()),
132            post: post.map(|op| op.into()),
133            delete: delete.map(|op| op.into()),
134            options: options.map(|op| op.into()),
135            head: head.map(|op| op.into()),
136            patch: patch.map(|op| op.into()),
137            trace: None,
138            servers: vec![],
139            parameters: parameters
140                .unwrap_or_default()
141                .into_iter()
142                .flat_map(|p| p.try_into().ok())
143                .collect(),
144            extensions,
145        })
146    }
147}
148
149/// Change something like "#/definitions/User" to "#/components/schemas/User"
150fn rewrite_ref(s: &str) -> String {
151    s.replace("#/definitions/", "#/components/schemas/")
152}
153
154fn build_schema_kind(type_: &str, format: Option<String>) -> v3::SchemaKind {
155    match type_ {
156        "string" => v3::SchemaKind::Type(v3::Type::String(v3::StringType {
157            format: {
158                let s = serde_json::to_string(&format).unwrap();
159                serde_json::from_str(&s).unwrap()
160            },
161            ..v3::StringType::default()
162        })),
163        "number" => v3::SchemaKind::Type(v3::Type::Number(v3::NumberType {
164            format: {
165                let s = serde_json::to_string(&format).unwrap();
166                serde_json::from_str(&s).unwrap()
167            },
168            ..v3::NumberType::default()
169        })),
170        "integer" => v3::SchemaKind::Type(v3::Type::Integer(v3::IntegerType {
171            format: {
172                let s = serde_json::to_string(&format).unwrap();
173                serde_json::from_str(&s).unwrap()
174            },
175            ..v3::IntegerType::default()
176        })),
177        "boolean" => v3::SchemaKind::Type(v3::Type::Boolean {}),
178        "array" => v3::SchemaKind::Type(v3::Type::Array(v3::ArrayType {
179            ..v3::ArrayType::default()
180        })),
181        "object" => {
182            let object_type = v3::ObjectType::default();
183            v3::SchemaKind::Type(v3::Type::Object(object_type))
184        }
185        _ => panic!("Unknown schema type: {}", type_),
186    }
187}
188
189impl Into<v3::Schema> for v2::Schema {
190    fn into(self) -> v3::Schema {
191        let v2::Schema {
192            description,
193            schema_type,
194            format,
195            enum_values,
196            required,
197            items,
198            properties,
199            all_of,
200            other,
201        } = self;
202
203        let schema_data = v3::SchemaData {
204            description,
205            extensions: other,
206            ..v3::SchemaData::default()
207        };
208
209        if let Some(all_of) = all_of {
210            return v3::Schema {
211                data: schema_data,
212                kind: v3::SchemaKind::AllOf {
213                    all_of: all_of.into_iter().map(|s| s.into()).collect(),
214                },
215            };
216        }
217
218        let schema_type = schema_type.unwrap_or_else(|| "object".to_string());
219        let mut schema_kind = build_schema_kind(&schema_type, format);
220
221        match &mut schema_kind {
222            v3::SchemaKind::Type(v3::Type::String(ref mut s)) => {
223                s.enumeration = enum_values.unwrap_or_default();
224            }
225            v3::SchemaKind::Type(v3::Type::Object(ref mut o)) => {
226                if let Some(properties) = properties {
227                    o.properties = properties.into_iter().map(|(k, v)| (k, v.into())).collect();
228                }
229                o.required = required.unwrap_or_default();
230            }
231            v3::SchemaKind::Type(v3::Type::Array(ref mut a)) => {
232                a.items = Some({
233                    let item = items.unwrap();
234                    let item = *item;
235                    let item: v3::RefOr<v3::Schema> = item.into();
236                    item.boxed()
237                });
238            }
239            _ => {}
240        }
241
242        v3::Schema {
243            data: schema_data,
244            kind: schema_kind,
245        }
246    }
247}
248
249impl TryInto<v3::RefOr<v3::Parameter>> for v2::Parameter {
250    type Error = anyhow::Error;
251
252    fn try_into(self) -> Result<v3::RefOr<v3::Parameter>, Self::Error> {
253        if !self.valid_v3_location() {
254            return Err(anyhow::anyhow!(
255                "Invalid location: {}",
256                serde_json::to_string(&self.location).unwrap()
257            ));
258        }
259        let v2::Parameter {
260            name,
261            location,
262            description,
263            required,
264            schema: _,
265            type_,
266            format,
267            items,
268            default,
269            unique_items,
270            collection_format,
271        } = self;
272        let type_ = type_.unwrap();
273
274        let mut kind = build_schema_kind(&type_, format);
275        let mut data = v3::SchemaData::default();
276
277        match &mut kind {
278            v3::SchemaKind::Type(v3::Type::Array(ref mut a)) => {
279                a.items = items.map(|item| {
280                    let item: v3::RefOr<v3::Schema> = item.into();
281                    item.boxed()
282                });
283                a.unique_items = unique_items.unwrap_or_default();
284            }
285            _ => {}
286        }
287        data.default = default;
288
289        let mut explode = None;
290        if let Some(collection_format) = collection_format {
291            match collection_format.as_str() {
292                "multi" => explode = Some(true),
293                "csv" => explode = Some(false),
294                _ => {}
295            }
296        }
297
298        let schema = v3::Schema { data, kind };
299        let data = v3::ParameterData {
300            name,
301            description,
302            required: required.unwrap_or_default(),
303            deprecated: None,
304            format: v3::ParameterSchemaOrContent::Schema(schema.into()),
305            example: None,
306            examples: Default::default(),
307            explode,
308            extensions: Default::default(),
309        };
310        let kind = match location {
311            v2::ParameterLocation::Query => v3::ParameterKind::Query {
312                allow_reserved: false,
313                style: Default::default(),
314                allow_empty_value: None,
315            },
316            v2::ParameterLocation::Header => v3::ParameterKind::Header {
317                style: Default::default(),
318            },
319            v2::ParameterLocation::Path => v3::ParameterKind::Path {
320                style: Default::default(),
321            },
322            v2::ParameterLocation::FormData | v2::ParameterLocation::Body => {
323                panic!("Invalid location")
324            }
325        };
326        let parameter = Parameter { data, kind };
327        Ok(v3::RefOr::Item(parameter))
328    }
329}
330
331fn split_params_into_params_and_body(
332    params: Option<Vec<v2::Parameter>>,
333) -> (Vec<v2::Parameter>, Vec<v2::Parameter>) {
334    params
335        .unwrap_or_default()
336        .into_iter()
337        .partition(|p| p.valid_v3_location())
338}
339
340impl Into<v3::Operation> for v2::Operation {
341    fn into(self) -> v3::Operation {
342        let v2::Operation {
343            consumes: _,
344            produces: _,
345            schemes: _,
346            tags,
347            summary,
348            description,
349            operation_id,
350            parameters,
351            mut responses,
352            security,
353            extensions,
354        } = self;
355        let (parameters, body) = split_params_into_params_and_body(parameters);
356        let body = body.into();
357
358        let responses = {
359            let mut r = v3::Responses::default();
360            r.default = responses.swap_remove("default").map(|r| r.into());
361            r.responses = responses
362                .into_iter()
363                .map(|(k, v)| {
364                    (
365                        StatusCode::Code(
366                            k.parse::<u16>()
367                                .expect(&format!("Invalid status code: {}", k)),
368                        ),
369                        v.into(),
370                    )
371                })
372                .collect();
373            r
374        };
375        v3::Operation {
376            tags: tags.unwrap_or_default(),
377            summary,
378            description,
379            external_docs: None,
380            operation_id,
381            parameters: parameters
382                .into_iter()
383                .flat_map(|p| p.try_into().ok())
384                .collect(),
385            request_body: Some(v3::RefOr::Item(body)),
386            responses,
387            deprecated: false,
388            security,
389            servers: vec![],
390            extensions,
391        }
392    }
393}
394
395impl Into<v3::RefOr<v3::Schema>> for v2::ReferenceOrSchema {
396    fn into(self) -> v3::RefOr<v3::Schema> {
397        match self {
398            v2::ReferenceOrSchema::Item(s) => v3::RefOr::Item(s.into()),
399            v2::ReferenceOrSchema::Reference { reference } => v3::RefOr::Reference {
400                reference: rewrite_ref(&reference),
401            },
402        }
403    }
404}
405
406impl Into<v3::RequestBody> for Vec<v2::Parameter> {
407    fn into(self) -> v3::RequestBody {
408        let mut object = v3::ObjectType::default();
409        for param in self {
410            let v2::Parameter {
411                name,
412                location,
413                description: _,
414                required,
415                schema,
416                type_: _,
417                format: _,
418                items: _,
419                default: _,
420                unique_items: _,
421                collection_format: _,
422            } = param;
423            assert!(location == v2::ParameterLocation::Body);
424            if required.unwrap_or_default() {
425                object.required.push(name.clone());
426            }
427            let schema = match schema {
428                Some(s) => s.into(),
429                None => v3::RefOr::Item(v3::Schema::new_any()),
430            };
431            object.properties.insert(name, schema);
432        }
433
434        let mut content = IndexMap::new();
435        content.insert(
436            "application/json".to_string(),
437            v3::MediaType {
438                schema: Some(v3::RefOr::Item(v3::Schema {
439                    data: v3::SchemaData::default(),
440                    kind: v3::SchemaKind::Type(v3::Type::Object(object)),
441                })),
442                ..v3::MediaType::default()
443            },
444        );
445        v3::RequestBody {
446            description: None,
447            content,
448            required: true,
449            extensions: Default::default(),
450        }
451    }
452}
453
454impl Into<v3::ExternalDocumentation> for v2::ExternalDoc {
455    fn into(self) -> v3::ExternalDocumentation {
456        let v2::ExternalDoc { description, url } = self;
457        v3::ExternalDocumentation {
458            description,
459            url,
460            ..v3::ExternalDocumentation::default()
461        }
462    }
463}
464
465impl Into<v3::Tag> for v2::Tag {
466    fn into(self) -> v3::Tag {
467        let v2::Tag {
468            name,
469            description,
470            external_docs,
471        } = self;
472        v3::Tag {
473            name,
474            description,
475            external_docs: external_docs
476                .and_then(|mut e| e.try_remove(0))
477                .map(|e| e.into()),
478            extensions: Default::default(),
479        }
480    }
481}
482
483impl Into<v3::Info> for v2::Info {
484    fn into(self) -> v3::Info {
485        let v2::Info {
486            title,
487            description,
488            terms_of_service,
489            contact,
490            license,
491            version,
492        } = self;
493        v3::Info {
494            title: title.unwrap_or_default(),
495            description,
496            terms_of_service,
497            contact: contact.map(|c| c.into()),
498            license: license.map(|l| l.into()),
499            version: version.unwrap_or_else(|| "0.1.0".to_string()),
500            extensions: Default::default(),
501        }
502    }
503}
504
505impl Into<v3::Contact> for v2::Contact {
506    fn into(self) -> v3::Contact {
507        let v2::Contact { name, url, email } = self;
508        v3::Contact {
509            name,
510            url,
511            email,
512            extensions: Default::default(),
513        }
514    }
515}
516
517impl Into<v3::License> for v2::License {
518    fn into(self) -> v3::License {
519        let v2::License { name, url } = self;
520        v3::License {
521            name: name.unwrap_or_default(),
522            url,
523            extensions: Default::default(),
524        }
525    }
526}
527
528impl Into<v3::RefOr<v3::SecurityScheme>> for v2::Security {
529    fn into(self) -> v3::RefOr<v3::SecurityScheme> {
530        match self {
531            v2::Security::ApiKey {
532                name,
533                location,
534                description,
535            } => {
536                let location = match location {
537                    v2::ApiKeyLocation::Query => v3::APIKeyLocation::Query,
538                    v2::ApiKeyLocation::Header => v3::APIKeyLocation::Header,
539                };
540                v3::RefOr::Item(v3::SecurityScheme::APIKey {
541                    location,
542                    name,
543                    description,
544                })
545            }
546            v2::Security::Basic { description } => v3::RefOr::Item(v3::SecurityScheme::HTTP {
547                scheme: "basic".to_string(),
548                bearer_format: None,
549                description,
550            }),
551            v2::Security::Oauth2 {
552                flow,
553                authorization_url,
554                token_url,
555                scopes,
556                description,
557            } => {
558                let mut implicit = None;
559                let mut password = None;
560                let mut client_credentials = None;
561                let mut authorization_code = None;
562                match flow {
563                    v2::Flow::AccessCode => {
564                        authorization_code = Some(v3::AuthCodeOAuth2Flow {
565                            authorization_url,
566                            token_url: token_url.unwrap(),
567                            refresh_url: None,
568                            scopes,
569                        });
570                    }
571                    v2::Flow::Application => {
572                        client_credentials = Some(v3::OAuth2Flow {
573                            token_url: token_url.unwrap(),
574                            refresh_url: None,
575                            scopes,
576                        });
577                    }
578                    v2::Flow::Implicit => {
579                        implicit = Some(v3::ImplicitOAuth2Flow {
580                            authorization_url,
581                            refresh_url: None,
582                            scopes,
583                        });
584                    }
585                    v2::Flow::Password => {
586                        password = Some(v3::OAuth2Flow {
587                            token_url: token_url.unwrap(),
588                            refresh_url: None,
589                            scopes,
590                        });
591                    }
592                }
593                let flows = v3::OAuth2Flows {
594                    implicit,
595                    password,
596                    client_credentials,
597                    authorization_code,
598                };
599                v3::RefOr::Item(v3::SecurityScheme::OAuth2 { flows, description })
600            }
601        }
602    }
603}
604
605impl Into<v3::RefOr<v3::Response>> for v2::Response {
606    fn into(self) -> v3::RefOr<v3::Response> {
607        let v2::Response {
608            description,
609            schema,
610        } = self;
611        let Some(schema) = schema else {
612            return v3::RefOr::Item(v3::Response {
613                description,
614                ..v3::Response::default()
615            });
616        };
617        v3::RefOr::Item(v3::Response {
618            description,
619            content: {
620                let mut map = IndexMap::new();
621                map.insert(
622                    "application/json".to_string(),
623                    v3::MediaType {
624                        schema: Some(schema.into()),
625                        ..v3::MediaType::default()
626                    },
627                );
628                map
629            },
630            ..v3::Response::default()
631        })
632    }
633}