openapiv3/v2/
schema.rs

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