schemadoc_diff/
schema.rs

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