openapiv3/v2/
upgrade.rs

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