jtd_derive/
schema.rs

1//! The internal Rust representation of a [_JSON Typedef_](https://jsontypedef.com/)
2//! schema.
3
4use std::collections::BTreeMap;
5
6use serde::Serialize;
7
8// All this corresponds fairly straightforwardly to https://jsontypedef.com/docs/jtd-in-5-minutes/
9// I'd normally try to separate the serialization logic from the Rust representation, but using
10// serde derives makes this so very easy. Damnit.
11
12/// The top level of a [_JSON Typedef_](https://jsontypedef.com/) schema.
13#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
14pub struct RootSchema {
15    /// The top-level
16    /// [definitions](https://jsontypedef.com/docs/jtd-in-5-minutes/#ref-schemas).
17    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
18    pub definitions: BTreeMap<String, Schema>,
19    /// The top-level schema.
20    #[serde(flatten)]
21    pub schema: Schema,
22}
23
24/// A [_JSON Typedef_](https://jsontypedef.com/) schema.
25#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
26pub struct Schema {
27    /// The [metadata](https://jsontypedef.com/docs/jtd-in-5-minutes/#the-metadata-keyword).
28    #[serde(skip_serializing_if = "Metadata::is_empty")]
29    pub metadata: Metadata,
30    /// The actual schema.
31    #[serde(flatten)]
32    pub ty: SchemaType,
33    /// Whether this schema is nullable.
34    #[serde(skip_serializing_if = "std::ops::Not::not")]
35    pub nullable: bool,
36}
37
38impl Default for Schema {
39    /// Provides an [empty schema](https://jsontypedef.com/docs/jtd-in-5-minutes/#empty-schemas).
40    /// Empty schemas accept any JSON data.
41    fn default() -> Self {
42        Self {
43            metadata: Metadata::default(),
44            ty: SchemaType::Empty,
45            nullable: false,
46        }
47    }
48}
49
50/// The 8 forms a schema can take. For more info
51/// [see here](https://jsontypedef.com/docs/jtd-in-5-minutes/#what-is-a-json-type-definition-schema).
52#[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        // Can only contain non-nullable "properties" schemas
80        mapping: BTreeMap<&'static str, Schema>,
81    },
82    Ref {
83        r#ref: String,
84    },
85}
86
87/// Typedef primitive types. See [the Typedef docs entry](https://jsontypedef.com/docs/jtd-in-5-minutes/#type-schemas).
88#[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/// Schema [metadata](https://jsontypedef.com/docs/jtd-in-5-minutes/#the-metadata-keyword).
123///
124/// Metadata is a freeform map and a way to extend Typedef. The spec doesn't specify
125/// what might go in there. By default, `jtd_derive` doesn't generate any metadata.
126#[derive(Default, Debug, PartialEq, Eq, Clone, Serialize)]
127pub struct Metadata(BTreeMap<&'static str, serde_json::Value>);
128
129impl Metadata {
130    /// Construct a [`Metadata`] object from something that can be converted
131    /// to the appropriate hashmap.
132    pub fn from_map(m: impl Into<BTreeMap<&'static str, serde_json::Value>>) -> Self {
133        Self(m.into())
134    }
135
136    /// Returns `true` if there are no metadata entries.
137    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}