1use std::collections::BTreeMap;
5
6use serde::Serialize;
7
8#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
14pub struct RootSchema {
15 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
18 pub definitions: BTreeMap<String, Schema>,
19 #[serde(flatten)]
21 pub schema: Schema,
22}
23
24#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
26pub struct Schema {
27 #[serde(skip_serializing_if = "Metadata::is_empty")]
29 pub metadata: Metadata,
30 #[serde(flatten)]
32 pub ty: SchemaType,
33 #[serde(skip_serializing_if = "std::ops::Not::not")]
35 pub nullable: bool,
36}
37
38impl Default for Schema {
39 fn default() -> Self {
42 Self {
43 metadata: Metadata::default(),
44 ty: SchemaType::Empty,
45 nullable: false,
46 }
47 }
48}
49
50#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
53#[serde(untagged)]
54pub enum SchemaType {
55 Empty,
56 Type {
57 r#type: TypeSchema,
58 },
59 Enum {
60 r#enum: Vec<&'static str>,
61 },
62 Elements {
63 elements: Box<Schema>,
64 },
65 #[serde(rename_all = "camelCase")]
66 Properties {
67 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
68 properties: BTreeMap<&'static str, Schema>,
69 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
70 optional_properties: BTreeMap<&'static str, Schema>,
71 #[serde(skip_serializing_if = "std::ops::Not::not")]
72 additional_properties: bool,
73 },
74 Values {
75 values: Box<Schema>,
76 },
77 Discriminator {
78 discriminator: &'static str,
79 mapping: BTreeMap<&'static str, Schema>,
81 },
82 Ref {
83 r#ref: String,
84 },
85}
86
87#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
89#[serde(rename_all = "snake_case")]
90pub enum TypeSchema {
91 Boolean,
92 String,
93 Timestamp,
94 Float32,
95 Float64,
96 Int8,
97 Uint8,
98 Int16,
99 Uint16,
100 Int32,
101 Uint32,
102}
103
104impl TypeSchema {
105 pub const fn name(&self) -> &'static str {
106 match self {
107 TypeSchema::Boolean => "boolean",
108 TypeSchema::String => "string",
109 TypeSchema::Timestamp => "timestamp",
110 TypeSchema::Float32 => "float32",
111 TypeSchema::Float64 => "float64",
112 TypeSchema::Int8 => "int8",
113 TypeSchema::Uint8 => "uint8",
114 TypeSchema::Int16 => "int16",
115 TypeSchema::Uint16 => "uint16",
116 TypeSchema::Int32 => "int32",
117 TypeSchema::Uint32 => "uint32",
118 }
119 }
120}
121
122#[derive(Default, Debug, PartialEq, Eq, Clone, Serialize)]
127pub struct Metadata(BTreeMap<&'static str, serde_json::Value>);
128
129impl Metadata {
130 pub fn from_map(m: impl Into<BTreeMap<&'static str, serde_json::Value>>) -> Self {
133 Self(m.into())
134 }
135
136 pub fn is_empty(&self) -> bool {
138 self.0.is_empty()
139 }
140}
141
142impl<A> Extend<A> for Metadata
143where
144 BTreeMap<&'static str, serde_json::Value>: Extend<A>,
145{
146 fn extend<T: IntoIterator<Item = A>>(&mut self, iter: T) {
147 self.0.extend(iter)
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use serde_json::json;
154
155 use super::*;
156
157 #[test]
158 fn empty() {
159 let repr = RootSchema {
160 schema: Schema {
161 ty: SchemaType::Empty,
162 ..Schema::default()
163 },
164 definitions: BTreeMap::new(),
165 };
166
167 assert_eq!(serde_json::to_value(&repr).unwrap(), serde_json::json!({}))
168 }
169
170 #[test]
171 fn primitive() {
172 let repr = RootSchema {
173 schema: Schema {
174 ty: SchemaType::Type {
175 r#type: TypeSchema::Int16,
176 },
177 ..Schema::default()
178 },
179 definitions: BTreeMap::new(),
180 };
181
182 assert_eq!(
183 serde_json::to_value(&repr).unwrap(),
184 serde_json::json!({"type": "int16"})
185 );
186 }
187
188 #[test]
189 fn nullable() {
190 let repr = RootSchema {
191 schema: Schema {
192 ty: SchemaType::Type {
193 r#type: TypeSchema::Int16,
194 },
195 nullable: true,
196 ..Schema::default()
197 },
198 definitions: BTreeMap::new(),
199 };
200
201 assert_eq!(
202 serde_json::to_value(&repr).unwrap(),
203 serde_json::json!({"type": "int16", "nullable": true})
204 );
205 }
206
207 #[test]
208 fn metadata() {
209 let repr = RootSchema {
210 schema: Schema {
211 metadata: Metadata::from_map([
212 ("desc", json!("a really nice type! 10/10")),
213 ("vec", json!([1, 2, 3])),
214 ]),
215 ty: SchemaType::Type {
216 r#type: TypeSchema::Int16,
217 },
218 nullable: false,
219 },
220 definitions: BTreeMap::new(),
221 };
222
223 assert_eq!(
224 serde_json::to_value(&repr).unwrap(),
225 serde_json::json!({"type": "int16", "metadata": {"desc": "a really nice type! 10/10", "vec": [1, 2, 3]}})
226 );
227 }
228
229 #[test]
230 fn r#enum() {
231 let repr = RootSchema {
232 schema: Schema {
233 ty: SchemaType::Enum {
234 r#enum: vec!["FOO", "BAR", "BAZ"],
235 },
236 ..Schema::default()
237 },
238 definitions: BTreeMap::new(),
239 };
240
241 assert_eq!(
242 serde_json::to_value(&repr).unwrap(),
243 serde_json::json!({ "enum": ["FOO", "BAR", "BAZ" ]})
244 )
245 }
246
247 #[test]
248 fn elements() {
249 let repr = RootSchema {
250 schema: Schema {
251 ty: SchemaType::Elements {
252 elements: Box::new(Schema {
253 ty: SchemaType::Enum {
254 r#enum: vec!["FOO", "BAR", "BAZ"],
255 },
256 nullable: true,
257 ..Schema::default()
258 }),
259 },
260 ..Schema::default()
261 },
262 definitions: BTreeMap::new(),
263 };
264
265 assert_eq!(
266 serde_json::to_value(&repr).unwrap(),
267 serde_json::json!({ "elements": { "enum": ["FOO", "BAR", "BAZ" ], "nullable": true} })
268 )
269 }
270
271 #[test]
272 fn properties() {
273 let repr = RootSchema {
274 schema: Schema {
275 ty: SchemaType::Properties {
276 properties: [
277 (
278 "name",
279 Schema {
280 ty: SchemaType::Type {
281 r#type: TypeSchema::String,
282 },
283 ..Schema::default()
284 },
285 ),
286 (
287 "isAdmin",
288 Schema {
289 ty: SchemaType::Type {
290 r#type: TypeSchema::Boolean,
291 },
292 ..Schema::default()
293 },
294 ),
295 ]
296 .into(),
297 optional_properties: [].into(),
298 additional_properties: false,
299 },
300 ..Schema::default()
301 },
302 definitions: BTreeMap::new(),
303 };
304
305 assert_eq!(
306 serde_json::to_value(&repr).unwrap(),
307 serde_json::json!({
308 "properties": {
309 "name": { "type": "string" },
310 "isAdmin": { "type": "boolean" }
311 }
312 })
313 )
314 }
315
316 #[test]
317 fn properties_extra_additional() {
318 let repr = RootSchema {
319 schema: Schema {
320 ty: SchemaType::Properties {
321 properties: [
322 (
323 "name",
324 Schema {
325 ty: SchemaType::Type {
326 r#type: TypeSchema::String,
327 },
328 ..Schema::default()
329 },
330 ),
331 (
332 "isAdmin",
333 Schema {
334 ty: SchemaType::Type {
335 r#type: TypeSchema::Boolean,
336 },
337 ..Schema::default()
338 },
339 ),
340 ]
341 .into(),
342 optional_properties: [(
343 "middleName",
344 Schema {
345 ty: SchemaType::Type {
346 r#type: TypeSchema::String,
347 },
348 ..Schema::default()
349 },
350 )]
351 .into(),
352 additional_properties: true,
353 },
354 ..Schema::default()
355 },
356 definitions: BTreeMap::new(),
357 };
358
359 assert_eq!(
360 serde_json::to_value(&repr).unwrap(),
361 serde_json::json!({
362 "properties": {
363 "name": { "type": "string" },
364 "isAdmin": { "type": "boolean" }
365 },
366 "optionalProperties": {
367 "middleName": { "type": "string" }
368 },
369 "additionalProperties": true
370 })
371 )
372 }
373
374 #[test]
375 fn values() {
376 let repr = RootSchema {
377 schema: Schema {
378 ty: SchemaType::Values {
379 values: Box::new(Schema {
380 ty: SchemaType::Type {
381 r#type: TypeSchema::Boolean,
382 },
383 ..Schema::default()
384 }),
385 },
386 ..Schema::default()
387 },
388 definitions: BTreeMap::new(),
389 };
390
391 assert_eq!(
392 serde_json::to_value(&repr).unwrap(),
393 serde_json::json!({ "values": { "type": "boolean" }})
394 )
395 }
396
397 #[test]
398 fn discriminator() {
399 let repr = RootSchema {
400 schema: Schema {
401 ty: SchemaType::Discriminator {
402 discriminator: "eventType",
403 mapping: [
404 (
405 "USER_CREATED",
406 Schema {
407 ty: SchemaType::Properties {
408 properties: [(
409 "id",
410 Schema {
411 ty: SchemaType::Type {
412 r#type: TypeSchema::String,
413 },
414 ..Schema::default()
415 },
416 )]
417 .into(),
418 optional_properties: [].into(),
419 additional_properties: false,
420 },
421 ..Schema::default()
422 },
423 ),
424 (
425 "USER_PAYMENT_PLAN_CHANGED",
426 Schema {
427 ty: SchemaType::Properties {
428 properties: [
429 (
430 "id",
431 Schema {
432 ty: SchemaType::Type {
433 r#type: TypeSchema::String,
434 },
435 ..Schema::default()
436 },
437 ),
438 (
439 "plan",
440 Schema {
441 ty: SchemaType::Enum {
442 r#enum: vec!["FREE", "PAID"],
443 },
444 ..Schema::default()
445 },
446 ),
447 ]
448 .into(),
449 optional_properties: [].into(),
450 additional_properties: false,
451 },
452 ..Schema::default()
453 },
454 ),
455 (
456 "USER_DELETED",
457 Schema {
458 ty: SchemaType::Properties {
459 properties: [
460 (
461 "id",
462 Schema {
463 ty: SchemaType::Type {
464 r#type: TypeSchema::String,
465 },
466 ..Schema::default()
467 },
468 ),
469 (
470 "softDelete",
471 Schema {
472 ty: SchemaType::Type {
473 r#type: TypeSchema::Boolean,
474 },
475 ..Schema::default()
476 },
477 ),
478 ]
479 .into(),
480 optional_properties: [].into(),
481 additional_properties: false,
482 },
483 ..Schema::default()
484 },
485 ),
486 ]
487 .into(),
488 },
489 ..Schema::default()
490 },
491 definitions: BTreeMap::new(),
492 };
493
494 assert_eq!(
495 serde_json::to_value(&repr).unwrap(),
496 serde_json::json!({
497 "discriminator": "eventType",
498 "mapping": {
499 "USER_CREATED": {
500 "properties": {
501 "id": { "type": "string" }
502 }
503 },
504 "USER_PAYMENT_PLAN_CHANGED": {
505 "properties": {
506 "id": { "type": "string" },
507 "plan": { "enum": ["FREE", "PAID"]}
508 }
509 },
510 "USER_DELETED": {
511 "properties": {
512 "id": { "type": "string" },
513 "softDelete": { "type": "boolean" }
514 }
515 }
516 }
517 })
518 )
519 }
520
521 #[test]
522 fn r#ref() {
523 let repr = RootSchema {
524 schema: Schema {
525 ty: SchemaType::Properties {
526 properties: [
527 (
528 "userLoc",
529 Schema {
530 ty: SchemaType::Ref {
531 r#ref: "coordinates".to_string(),
532 },
533 ..Schema::default()
534 },
535 ),
536 (
537 "serverLoc",
538 Schema {
539 ty: SchemaType::Ref {
540 r#ref: "coordinates".to_string(),
541 },
542 ..Schema::default()
543 },
544 ),
545 ]
546 .into(),
547 optional_properties: [].into(),
548 additional_properties: false,
549 },
550 ..Schema::default()
551 },
552 definitions: [(
553 "coordinates".to_string(),
554 Schema {
555 ty: SchemaType::Properties {
556 properties: [
557 (
558 "lat",
559 Schema {
560 ty: SchemaType::Type {
561 r#type: TypeSchema::Float32,
562 },
563 ..Schema::default()
564 },
565 ),
566 (
567 "lng",
568 Schema {
569 ty: SchemaType::Type {
570 r#type: TypeSchema::Float32,
571 },
572 ..Schema::default()
573 },
574 ),
575 ]
576 .into(),
577 optional_properties: [].into(),
578 additional_properties: false,
579 },
580 ..Schema::default()
581 },
582 )]
583 .into(),
584 };
585
586 assert_eq!(
587 serde_json::to_value(&repr).unwrap(),
588 serde_json::json!({
589 "definitions": {
590 "coordinates": {
591 "properties": {
592 "lat": { "type": "float32" },
593 "lng": { "type": "float32" }
594 }
595 }
596 },
597 "properties": {
598 "userLoc": { "ref": "coordinates" },
599 "serverLoc": { "ref": "coordinates" }
600 }
601 })
602 )
603 }
604}