schemadoc_diff/schemas/openapi303/
schema.rs

1use serde::{Deserialize, Serialize};
2use std::fmt::Debug;
3
4use indexmap::IndexMap;
5use serde_json::Value;
6
7use crate::core::{DiffResult, Either, MayBeRefCore, ReferenceDescriptor};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct OpenApi303Ref {
11    #[serde(rename = "$ref")]
12    pub reference: String,
13}
14
15impl ReferenceDescriptor for OpenApi303Ref {
16    fn reference(&self) -> &str {
17        &self.reference
18    }
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct OpenApi303RefDiff {
23    #[serde(rename = "$ref")]
24    pub reference: DiffResult<String>,
25}
26
27impl ReferenceDescriptor for OpenApi303RefDiff {
28    fn reference(&self) -> &str {
29        self.reference.get().expect("Reference diff cannot be null")
30    }
31}
32
33pub type MayBeRef303<T> = MayBeRefCore<T, OpenApi303Ref>;
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase")]
37pub struct OpenApi303 {
38    pub openapi: String,
39    pub info: Option<Info>,
40    pub servers: Option<Vec<Server>>,
41    pub paths: Option<IndexMap<String, MayBeRef303<Path>>>,
42    pub components: Option<Components>,
43    // TODO:
44    // pub security:
45    pub tags: Option<Vec<Tag>>,
46    pub external_docs: Option<ExternalDoc>,
47}
48
49impl OpenApi303 {
50    pub const fn id() -> &'static str {
51        "OpenApi303"
52    }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub struct Info {
58    pub title: Option<String>,
59    pub description: Option<String>,
60    pub terms_of_service: Option<String>,
61
62    pub contact: Option<Contact>,
63    pub license: Option<License>,
64
65    pub version: Option<String>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct Contact {
70    pub name: Option<String>,
71    pub url: Option<String>,
72    pub email: Option<String>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct License {
77    pub name: Option<String>,
78    pub url: Option<String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct Server {
83    pub url: Option<String>,
84    pub description: Option<String>,
85    pub variables: Option<IndexMap<String, ServerVariable>>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct ServerVariable {
90    pub r#enum: Option<Vec<String>>,
91    pub default: Option<Value>,
92    pub description: Option<String>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96#[serde(rename_all = "camelCase")]
97pub struct Components {
98    pub schemas: Option<IndexMap<String, MayBeRef303<Schema>>>,
99    pub responses: Option<IndexMap<String, MayBeRef303<Response>>>,
100    pub parameters: Option<IndexMap<String, MayBeRef303<Parameter>>>,
101    pub examples: Option<IndexMap<String, MayBeRef303<Example>>>,
102    pub request_bodies: Option<IndexMap<String, MayBeRef303<RequestBody>>>,
103    pub headers: Option<IndexMap<String, MayBeRef303<Header>>>,
104    pub security_schemes:
105        Option<IndexMap<String, MayBeRef303<SecurityScheme>>>,
106    pub links: Option<IndexMap<String, MayBeRef303<Link>>>,
107    // pub callbacks: Option<HashMap<String, MayBeRef303<Header>>>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ExternalDoc {
112    pub url: Option<String>,
113    pub description: Option<String>,
114}
115
116#[derive(Serialize, Deserialize, Debug, Clone, Default)]
117#[serde(rename_all = "camelCase")]
118pub struct Parameter {
119    pub name: String,
120    pub r#in: String,
121
122    pub description: Option<String>,
123
124    pub required: Option<bool>,
125    pub deprecated: Option<bool>,
126    pub allow_empty_value: Option<bool>,
127
128    pub style: Option<String>,
129    pub explode: Option<bool>,
130    pub allow_reserved: Option<bool>,
131
132    pub schema: Option<MayBeRef303<Schema>>,
133
134    pub example: Option<Value>,
135    pub examples: Option<IndexMap<String, MayBeRef303<Value>>>,
136
137    pub content: Option<IndexMap<String, MediaType>>,
138
139    #[serde(flatten)]
140    pub custom_fields: IndexMap<String, Value>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct RequestBody {
145    pub description: Option<String>,
146    pub content: Option<IndexMap<String, MediaType>>,
147    pub required: Option<bool>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct MediaType {
152    pub schema: Option<MayBeRef303<Schema>>,
153    pub example: Option<Value>,
154
155    pub examples: Option<IndexMap<String, MayBeRef303<Example>>>,
156    pub encoding: Option<IndexMap<String, Encoding>>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct Encoding {
162    pub content_type: Option<String>,
163    pub headers: Option<IndexMap<String, MayBeRef303<Header>>>,
164    pub style: Option<String>,
165    pub explode: Option<bool>,
166    pub allow_reserved: Option<bool>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
170#[serde(rename_all = "camelCase")]
171pub struct Link {
172    pub operation_ref: Option<String>,
173    pub operation_id: Option<String>,
174    pub parameters: Option<IndexMap<String, Value>>,
175    pub request_body: Option<Value>,
176    pub description: Option<String>,
177    pub server: Option<Server>,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct Response {
182    pub description: Option<String>,
183    pub headers: Option<IndexMap<String, MayBeRef303<Header>>>,
184    pub content: Option<IndexMap<String, MediaType>>,
185
186    pub links: Option<IndexMap<String, MayBeRef303<Link>>>,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190#[serde(rename_all = "camelCase")]
191pub struct Example {
192    pub summary: Option<String>,
193    pub description: Option<String>,
194    pub value: Option<Value>,
195    pub external_value: Option<String>,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub struct Discriminator {
201    pub property_name: Option<String>,
202    pub mapping: Option<IndexMap<String, String>>,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct Xml {
207    pub name: Option<String>,
208    pub namespace: Option<String>,
209    pub prefix: Option<String>,
210    pub attribute: Option<bool>,
211    pub wrapped: Option<bool>,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct SecurityScheme {
217    pub r#type: Option<String>,
218    pub description: Option<String>,
219    pub name: Option<String>,
220    pub r#in: Option<String>,
221    pub scheme: Option<String>,
222    pub bearer_format: Option<String>,
223    pub flows: Option<OAuthFlows>,
224    pub open_id_connect_url: Option<String>,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
228#[serde(rename_all = "camelCase")]
229pub struct OAuthFlows {
230    pub implicit: Option<OAuthFlow>,
231    pub password: Option<OAuthFlow>,
232    pub client_credentials: Option<OAuthFlow>,
233    pub authorization_code: Option<OAuthFlow>,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase")]
238pub struct OAuthFlow {
239    pub authorization_url: Option<String>,
240    pub token_url: Option<String>,
241    pub refresh_url: Option<String>,
242    pub scopes: Option<IndexMap<String, String>>,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
246#[serde(rename_all = "camelCase")]
247pub struct Tag {
248    pub name: Option<String>,
249    pub description: Option<String>,
250    pub external_doc: Option<ExternalDoc>,
251}
252
253#[derive(Debug, Clone, Default, Serialize, Deserialize)]
254#[serde(rename_all = "camelCase")]
255pub struct Schema {
256    pub title: Option<String>,
257    pub multiple_of: Option<f32>,
258    pub maximum: Option<f32>,
259    pub exclusive_maximum: Option<bool>,
260    pub minimum: Option<f32>,
261    pub exclusive_minimum: Option<bool>,
262    pub max_length: Option<usize>,
263    pub min_length: Option<usize>,
264    pub pattern: Option<String>,
265    pub max_items: Option<usize>,
266    pub min_items: Option<usize>,
267    pub unique_items: Option<bool>,
268    pub max_properties: Option<usize>,
269    pub min_properties: Option<usize>,
270    pub required: Option<Vec<String>>,
271    pub r#enum: Option<Vec<Value>>,
272
273    // Allow array of types, which is not corresponds to the schema version
274    pub r#type: Option<Either<String, Vec<String>>>,
275    pub all_of: Option<Vec<MayBeRef303<Schema>>>,
276    pub one_of: Option<Vec<MayBeRef303<Schema>>>,
277    pub any_of: Option<Vec<MayBeRef303<Schema>>>,
278    pub not: Option<Vec<MayBeRef303<Schema>>>,
279
280    pub items: Box<Option<MayBeRef303<Schema>>>,
281    pub properties: Option<IndexMap<String, MayBeRef303<Schema>>>,
282    pub additional_properties: Option<Either<bool, MayBeRef303<Schema>>>,
283    pub description: Option<String>,
284    pub format: Option<String>,
285    pub default: Option<Value>,
286
287    pub nullable: Option<bool>,
288    pub discriminator: Option<Discriminator>,
289    pub read_only: Option<bool>,
290    pub write_only: Option<bool>,
291    pub xml: Option<Xml>,
292    pub external_docs: Option<ExternalDoc>,
293    pub example: Option<Value>,
294    pub deprecated: Option<bool>,
295
296    #[serde(flatten)]
297    pub custom_fields: IndexMap<String, Value>,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
301#[serde(rename_all = "camelCase")]
302pub struct Header {
303    pub description: Option<String>,
304
305    pub required: Option<bool>,
306    pub deprecated: Option<bool>,
307    pub allow_empty_value: Option<bool>,
308
309    pub style: Option<String>,
310    pub explode: Option<bool>,
311    pub allow_reserved: Option<bool>,
312
313    pub schema: Option<MayBeRef303<Schema>>,
314
315    pub example: Option<Value>,
316    pub examples: Option<IndexMap<String, MayBeRef303<Value>>>,
317
318    pub content: Option<IndexMap<String, MediaType>>,
319
320    #[serde(flatten)]
321    pub custom_fields: IndexMap<String, Value>,
322}
323
324#[derive(Debug, Clone, Default, Serialize, Deserialize)]
325#[serde(rename_all = "camelCase")]
326pub struct Operation {
327    pub tags: Option<Vec<String>>,
328    pub summary: Option<String>,
329    pub description: Option<String>,
330
331    pub external_docs: Option<ExternalDoc>,
332
333    pub operation_id: Option<String>,
334
335    pub parameters: Option<Vec<MayBeRef303<Parameter>>>,
336
337    pub responses: Option<IndexMap<String, MayBeRef303<Response>>>,
338
339    pub request_body: Option<MayBeRef303<RequestBody>>,
340    pub servers: Option<Vec<Server>>,
341
342    pub security: Option<Vec<IndexMap<String, Vec<String>>>>,
343
344    // TODO:
345    // pub callbacks
346    pub deprecated: Option<bool>,
347}
348
349#[derive(Debug, Clone, Default, Serialize, Deserialize)]
350pub struct Path {
351    pub get: Option<Operation>,
352    pub put: Option<Operation>,
353    pub post: Option<Operation>,
354    pub delete: Option<Operation>,
355    pub options: Option<Operation>,
356    pub head: Option<Operation>,
357    pub patch: Option<Operation>,
358    pub trace: Option<Operation>,
359
360    pub servers: Option<Vec<Server>>,
361    pub parameters: Option<Vec<MayBeRef303<Parameter>>>,
362
363    pub summary: Option<String>,
364    pub description: Option<String>,
365}
366
367#[cfg(test)]
368mod tests {
369    use crate::core::Either;
370    use crate::schemas::openapi310::schema::*;
371
372    #[test]
373    fn check_operation() {
374        let op_def = r#"{
375      "post": {
376        "tags": ["Nodes"],
377        "summary": "Export Xlsx Template",
378        "description": "Generate XLSX-template for aggregated node data editing",
379        "operationId": "export_xlsx_template_api_v2_nodes__path__template_generate__post",
380        "parameters": [
381          {
382            "required": true,
383            "schema": { "title": "Path", "type": "string" },
384            "name": "path",
385            "in": "path"
386          },
387          {
388            "required": false,
389            "schema": { "title": "Update Sender", "type": "string" },
390            "name": "update_sender",
391            "in": "query"
392          },
393          {
394            "required": false,
395            "schema": { "title": "Force", "type": "boolean", "default": false },
396            "name": "force",
397            "in": "query"
398          },
399          {
400            "required": false,
401            "schema": { "title": "Compound Amount", "type": "integer" },
402            "name": "compound_amount",
403            "in": "query"
404          },
405          {
406            "required": false,
407            "schema": {
408              "allOf": [{ "$ref": "/components/schemas/ExportFmt" }],
409              "default": "xlsx"
410            },
411            "name": "export_format",
412            "in": "query"
413          }
414        ],
415        "requestBody": {
416          "content": {
417            "application/json": {
418              "schema": {
419                "$ref": "/components/schemas/Body_export_xlsx_template_api_v2_nodes__path__template_generate__post"
420              }
421            }
422          }
423        },
424        "responses": {
425          "200": {
426            "description": "Successful Response",
427            "content": {
428              "application/json": { "schema": {} },
429              "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {}
430            }
431          },
432          "422": {
433            "description": "Validation Error",
434            "content": {
435              "application/json": {
436                "schema": { "$ref": "/components/schemas/HTTPValidationError" }
437              }
438            }
439          }
440        },
441        "security": [{ "OAuth2PasswordBearer": [] }]
442      }
443    }"#;
444
445        let _: Path = serde_json::from_str(op_def).unwrap();
446    }
447
448    #[test]
449    fn check_schema_additional_properties() {
450        let op_def = r#"{
451            "title": "AdditionalProperties",
452            "type": "object",
453            "additionalProperties": {
454              "$ref": "/components/schemas/AdditionalProperties"
455            }
456          }"#;
457
458        let op: Schema = serde_json::from_str(op_def).unwrap();
459        assert!(matches!(op.additional_properties, Some(Either::Right(_))));
460
461        let op_def = r#"{
462            "title": "AdditionalProperties",
463            "type": "object",
464            "additionalProperties": false
465          }"#;
466
467        let op: Schema = serde_json::from_str(op_def).unwrap();
468        assert!(matches!(op.additional_properties, Some(Either::Left(_))));
469
470        let sc_def = r#"
471        {
472        "type": "object",
473        "discriminator": { "propertyName": "type" },
474        "properties": {
475          "type": {
476            "type": "string",
477            "description": "The type of context being attached to the entity.",
478            "enum": ["link", "image"]
479          }
480        },
481        "required": ["type"]
482      }
483        "#;
484        let op: Schema = serde_json::from_str(sc_def).unwrap();
485        assert!(matches!(op.discriminator, Some(_)))
486    }
487
488    #[test]
489    fn check_strange_thing() {
490        let op_def = r#"
491 {
492                "description": "Response",
493                "content": {
494                  "application/json": {
495                    "schema": {
496                      "allOf": [
497                        {
498                          "type": "object",
499                          "properties": {
500                            "client_id": {
501                              "type": "string"
502                            },
503                            "client_secret": {
504                              "type": "string"
505                            },
506                            "webhook_secret": {
507                              "type": [
508                                "string",
509                                "null"
510                              ]
511                            },
512                            "pem": {
513                              "type": "string"
514                            }
515                          },
516                          "required": [
517                            "client_id",
518                            "client_secret",
519                            "webhook_secret",
520                            "pem"
521                          ],
522                          "additionalProperties": true
523                        }
524                      ]
525                    },
526                    "examples": {
527                    }
528                  }
529                }
530              }
531        "#;
532
533        let source_de = &mut serde_json::Deserializer::from_str(op_def);
534        let result: Result<Response, _> =
535            serde_path_to_error::deserialize(source_de);
536        let _ = result.map_err(|err| {
537            let path = err.path().to_string();
538            dbg!(path, err)
539        });
540    }
541
542    #[test]
543    fn check_additional_props() {
544        let schema_def = r#"
545        {
546            "type": "array",
547            "items": {
548              "maxItems": 4,
549              "minItems": 4,
550              "type": "array",
551              "items": {
552                  "type": "string"
553                }
554            }
555          }
556        "#;
557
558        let _: Schema = serde_json::from_str(schema_def).unwrap();
559    }
560}