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 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 }
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 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 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 pub properties: Option<IndexMap<String, MayBeRef<Schema>>>,
341 pub additional_properties: Option<Either<bool, MayBeRef<Schema>>>, 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 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 }
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 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}