openapiv3/v2/
schema.rs

1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3
4// http://json.schemastore.org/swagger-2.0
5
6#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
7#[serde(rename_all = "lowercase")]
8pub enum Scheme {
9    Http,
10    Https,
11    Ws,
12    Wss,
13}
14
15impl Scheme {
16    pub fn as_str(&self) -> &'static str {
17        match self {
18            Scheme::Http => "http",
19            Scheme::Https => "https",
20            Scheme::Ws => "ws",
21            Scheme::Wss => "wss",
22        }
23    }
24}
25
26impl Default for Scheme {
27    fn default() -> Self {
28        Scheme::Http
29    }
30}
31
32/// top level document
33#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
34#[serde(rename_all = "camelCase")]
35pub struct OpenAPI {
36    /// The Swagger version of this document.
37    pub swagger: String,
38    pub info: Info,
39    /// The host (name or ip) of the API. Example: 'swagger.io'
40    /// ^[^{}/ :\\\\]+(?::\\d+)?$
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub host: Option<String>,
43    /// The base path to the API. Example: '/api'.
44    #[serde(skip_serializing_if = "Option::is_none")]
45    #[serde(rename = "basePath")]
46    pub base_path: Option<String>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub schemes: Option<Vec<Scheme>>,
49    /// A list of MIME types accepted by the API.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub consumes: Option<Vec<String>>,
52    /// A list of MIME types the API can produce.
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub produces: Option<Vec<String>>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub tags: Option<Vec<Tag>>,
57    /// Relative paths to the individual endpoints. They must be relative
58    /// to the 'basePath'.
59    pub paths: IndexMap<String, PathItem>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub definitions: Option<IndexMap<String, Schema>>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub parameters: Option<IndexMap<String, Parameter>>,
64    /// mappings to http response codes or "default"
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub responses: Option<IndexMap<String, Response>>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub security_definitions: Option<IndexMap<String, Security>>,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub security: Option<Vec<IndexMap<String, Vec<String>>>>,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub external_docs: Option<Vec<ExternalDoc>>,
73    #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")]
74    pub extensions: IndexMap<String, serde_json::Value>,
75}
76
77#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
78#[serde(rename_all = "lowercase")]
79pub struct Tag {
80    pub name: String,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub description: Option<String>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub external_docs: Option<Vec<ExternalDoc>>,
85}
86
87#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
88pub struct ExternalDoc {
89    pub url: String,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub description: Option<String>,
92}
93
94/// General information about the API.
95///
96/// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#info-object
97#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
98#[serde(rename_all = "lowercase")]
99pub struct Info {
100    /// A unique and precise title of the API.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub title: Option<String>,
103    /// A semantic version number of the API.
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub description: Option<String>,
106    #[serde(rename = "termsOfService", skip_serializing_if = "Option::is_none")]
107    pub terms_of_service: Option<String>,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub contact: Option<Contact>,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub license: Option<License>,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub version: Option<String>,
114}
115
116#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
117pub struct Contact {
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub name: Option<String>,
120    // TODO: Make sure the url is a valid URL
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub url: Option<String>,
123    // TODO: Make sure the email is a valid email
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub email: Option<String>,
126}
127
128/// todo x-* properties
129#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
130pub struct License {
131    /// The name of the license type. It's encouraged to use an OSI
132    /// compatible license.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub name: Option<String>,
135    /// The URL pointing to the license.
136    // TODO: Make sure the url is a valid URL
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub url: Option<String>,
139}
140
141/// todo support x-* properties
142#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
143pub struct PathItem {
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub get: Option<Operation>,
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub post: Option<Operation>,
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub put: Option<Operation>,
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub patch: Option<Operation>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub delete: Option<Operation>,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub options: Option<Operation>,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub head: Option<Operation>,
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub parameters: Option<Vec<Parameter>>,
160    #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")]
161    pub extensions: IndexMap<String, serde_json::Value>,
162}
163
164/// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object
165#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
166#[serde(rename_all = "lowercase")]
167pub struct Operation {
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub summary: Option<String>,
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub description: Option<String>,
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub consumes: Option<Vec<String>>,
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub produces: Option<Vec<String>>,
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub schemes: Option<Vec<String>>,
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub tags: Option<Vec<String>>,
180    #[serde(rename = "operationId", skip_serializing_if = "Option::is_none")]
181    pub operation_id: Option<String>,
182    pub responses: IndexMap<String, Response>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub parameters: Option<Vec<Parameter>>,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub security: Option<Vec<SecurityRequirement>>,
187    #[serde(flatten, deserialize_with = "crate::util::deserialize_extensions")]
188    pub extensions: IndexMap<String, serde_json::Value>,
189}
190
191/// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#securityRequirementObject
192pub type SecurityRequirement = IndexMap<String, Vec<String>>;
193
194#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
195#[serde(rename_all = "lowercase")]
196pub enum ParameterLocation {
197    Query,
198    Header,
199    Path,
200    FormData,
201    Body,
202}
203
204impl Default for ParameterLocation {
205    fn default() -> Self {
206        ParameterLocation::Query
207    }
208}
209
210impl Parameter {
211    pub fn valid_v3_location(&self) -> bool {
212        use ParameterLocation::*;
213        matches!(self.location, Query | Header | Path)
214    }
215}
216
217#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
218#[serde(rename_all = "camelCase")]
219pub struct Parameter {
220    pub name: String,
221    #[serde(rename = "in")]
222    pub location: ParameterLocation,
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub required: Option<bool>,
225    /// Only relevant for `ParameterLocation::Body`
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub schema: Option<ReferenceOrSchema>,
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub unique_items: Option<bool>,
230    #[serde(skip_serializing_if = "Option::is_none")]
231    #[serde(rename = "type")]
232    pub type_: Option<String>,
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub format: Option<String>,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub description: Option<String>,
237    /// Required if type is array.
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub items: Option<ReferenceOrSchema>,
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub default: Option<serde_json::Value>,
242    #[serde(skip_serializing_if = "Option::is_none")]
243    #[serde(rename = "collectionFormat")]
244    pub collection_format: Option<String>,
245}
246
247#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
248pub struct Response {
249    pub description: String,
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub schema: Option<ReferenceOrSchema>,
252}
253
254#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
255#[serde(rename_all = "lowercase")]
256pub enum ApiKeyLocation {
257    Query,
258    Header,
259}
260
261#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
262#[serde(tag = "type")]
263pub enum Security {
264    #[serde(rename = "apiKey")]
265    ApiKey {
266        name: String,
267        #[serde(rename = "in")]
268        location: ApiKeyLocation,
269        #[serde(skip_serializing_if = "Option::is_none")]
270        description: Option<String>,
271    },
272    #[serde(rename = "oauth2")]
273    Oauth2 {
274        flow: Flow,
275        #[serde(rename = "authorizationUrl")]
276        authorization_url: String,
277        #[serde(rename = "tokenUrl")]
278        #[serde(skip_serializing_if = "Option::is_none")]
279        token_url: Option<String>,
280        scopes: IndexMap<String, String>,
281        #[serde(skip_serializing_if = "Option::is_none")]
282        description: Option<String>,
283    },
284    #[serde(rename = "basic")]
285    Basic {
286        #[serde(skip_serializing_if = "Option::is_none")]
287        description: Option<String>,
288    },
289}
290
291#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
292#[serde(rename_all = "camelCase")]
293pub enum Flow {
294    Implicit,
295    Password,
296    Application,
297    AccessCode,
298}
299
300/// A [JSON schema](http://json-schema.org/) definition describing
301/// the shape and properties of an object.
302///
303/// This may also contain a `$ref` to another definition
304#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
305pub struct Schema {
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub description: Option<String>,
308    #[serde(skip_serializing_if = "Option::is_none")]
309    #[serde(rename = "type")]
310    pub schema_type: Option<String>,
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub format: Option<String>,
313    #[serde(skip_serializing_if = "Option::is_none")]
314    #[serde(rename = "enum")]
315    pub enum_values: Option<Vec<String>>,
316    #[serde(skip_serializing_if = "Option::is_none")]
317    pub required: Option<Vec<String>>,
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub items: Option<Box<ReferenceOrSchema>>,
320    // implies object
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub properties: Option<IndexMap<String, ReferenceOrSchema>>,
323    // composition
324    #[serde(skip_serializing_if = "Option::is_none")]
325    #[serde(rename = "allOf")]
326    pub all_of: Option<Vec<ReferenceOrSchema>>,
327    // TODO: we need a validation step that we only collect x-* properties here.
328    #[serde(flatten)]
329    pub other: IndexMap<String, serde_json::Value>,
330}
331
332#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
333#[serde(untagged)]
334pub enum ReferenceOrSchema {
335    Reference {
336        #[serde(rename = "$ref")]
337        reference: String,
338    },
339    Item(Schema),
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use serde_json;
346    use serde_yaml;
347
348    #[test]
349    fn security_api_deserializes() {
350        let json = r#"{"type":"apiKey", "name":"foo", "in": "query"}"#;
351        assert_eq!(
352            serde_yaml::from_str::<Security>(&json).unwrap(),
353            Security::ApiKey {
354                name: "foo".into(),
355                location: serde_json::from_str("\"query\"").unwrap(),
356                description: None,
357            }
358        );
359    }
360
361    #[test]
362    fn security_api_serializes() {
363        let json = r#"{"type":"apiKey","name":"foo","in":"query"}"#;
364        assert_eq!(
365            serde_json::to_string(&Security::ApiKey {
366                name: "foo".into(),
367                location: serde_json::from_str("\"query\"").unwrap(),
368                description: None,
369            })
370            .unwrap(),
371            json
372        );
373    }
374
375    #[test]
376    fn security_basic_deserializes() {
377        let json = r#"{"type":"basic"}"#;
378        assert_eq!(
379            serde_yaml::from_str::<Security>(&json).unwrap(),
380            Security::Basic { description: None }
381        );
382    }
383
384    #[test]
385    fn security_basic_serializes() {
386        let json = r#"{"type":"basic"}"#;
387        assert_eq!(
388            json,
389            serde_json::to_string(&Security::Basic { description: None }).unwrap()
390        );
391    }
392
393    #[test]
394    fn security_oauth_deserializes() {
395        let json = r#"{"type":"oauth2","flow":"implicit","authorizationUrl":"foo/bar","scopes":{"foo":"bar"}}"#;
396        let mut scopes = IndexMap::new();
397        scopes.insert("foo".into(), "bar".into());
398        assert_eq!(
399            serde_yaml::from_str::<Security>(&json).unwrap(),
400            Security::Oauth2 {
401                flow: Flow::Implicit,
402                authorization_url: "foo/bar".into(),
403                token_url: None,
404                scopes: scopes,
405                description: None,
406            }
407        );
408    }
409
410    #[test]
411    fn security_oauth_serializes() {
412        let json = r#"{"type":"oauth2","flow":"implicit","authorizationUrl":"foo/bar","scopes":{"foo":"bar"}}"#;
413        let mut scopes = IndexMap::new();
414        scopes.insert("foo".into(), "bar".into());
415        assert_eq!(
416            json,
417            serde_json::to_string(&Security::Oauth2 {
418                flow: Flow::Implicit,
419                authorization_url: "foo/bar".into(),
420                token_url: None,
421                scopes,
422                description: None,
423            })
424            .unwrap()
425        );
426    }
427
428    #[test]
429    fn parameter_or_ref_deserializes_ref() {
430        let json = r#"{"$ref":"foo/bar"}"#;
431        assert_eq!(
432            serde_yaml::from_str::<ReferenceOrSchema>(&json).unwrap(),
433            ReferenceOrSchema::Reference {
434                reference: "foo/bar".into()
435            }
436        );
437    }
438
439    #[test]
440    fn parameter_or_ref_serializes_pref() {
441        let json = r#"{"$ref":"foo/bar"}"#;
442        assert_eq!(
443            json,
444            serde_json::to_string(&ReferenceOrSchema::Reference {
445                reference: "foo/bar".into()
446            })
447            .unwrap()
448        );
449    }
450}