jtd/schema.rs
1use crate::SerdeSchema;
2use serde_json::Value;
3use std::collections::{BTreeMap, BTreeSet};
4use thiserror::Error;
5
6/// A convenience alias for the JSON Typedef `definitions` keyword value.
7pub type Definitions = BTreeMap<String, Schema>;
8
9/// A convenience alias for the JSON Typedef `metadata` keyword value.
10pub type Metadata = BTreeMap<String, Value>;
11
12/// A pattern-matching-friendly representation of a JSON Typedef schema.
13///
14/// Each variant of this schema corresponds to one of the eight "forms" a schema
15/// may take on. All of the forms share the following fields:
16///
17/// * `definitions` corresponds to the JSON Typedef keyword of the same name.
18/// This should only be non-empty on root schemas. Otherwise,
19/// [`Schema::validate`] will return
20/// [`SchemaValidateError::NonRootDefinitions`].
21///
22/// * `metadata` corresponds to the JSON Typedef keyword of the same name. Use
23/// this to convey information not pertinent to validation, such as hints for
24/// code generation. Do not expect other parties to understand the fields
25/// inside metadata unless you've agreed upon them out-of-band.
26///
27/// Except for [`Schema::Empty`], all of the forms also share one additional
28/// field:
29///
30/// * `nullable` corresponds to the JSON Typedef keyword of the same name. If
31/// set to "true", then regardless of any other considerations the schema will
32/// accept JSON `null` as valid.
33///
34/// [`Schema::Empty`] omits `nullable` because it's redundant; schemas of the
35/// empty form already accept `null` anyway.
36///
37/// For convenience, these three common properties have associated borrowing
38/// "getters": [`Schema::definitions`], [`Schema::metadata`], and
39/// [`Schema::nullable`].
40///
41/// If you are trying to parse a JSON Typedef schema from JSON, see
42/// [`SerdeSchema`] and [`Schema::from_serde_schema`].
43///
44/// ```
45/// use jtd::{SerdeSchema, Schema};
46/// use serde_json::json;
47///
48/// assert_eq!(
49/// Schema::from_serde_schema(serde_json::from_value(json!({
50/// "elements": {
51/// "type": "uint32",
52/// "nullable": true
53/// }
54/// })).unwrap()).unwrap(),
55/// jtd::Schema::Elements {
56/// definitions: Default::default(),
57/// metadata: Default::default(),
58/// nullable: false,
59/// elements: Box::new(jtd::Schema::Type {
60/// definitions: Default::default(),
61/// metadata: Default::default(),
62/// nullable: true,
63/// type_: jtd::Type::Uint32,
64/// })
65/// }
66/// );
67/// ```
68#[derive(Clone, Debug, PartialEq)]
69pub enum Schema {
70 /// The [empty](https://tools.ietf.org/html/rfc8927#section-2.2.1) form.
71 ///
72 /// The empty form will accept all inputs. It corresponds to the "top" type
73 /// of many programming language, like Java's `Object` or TypeScript's
74 /// `any`.
75 Empty {
76 definitions: Definitions,
77 metadata: Metadata,
78 },
79
80 /// The [ref](https://tools.ietf.org/html/rfc8927#section-2.2.2) form.
81 ///
82 /// The ref form accepts whatever the definition it refers to accepts.
83 Ref {
84 definitions: Definitions,
85 metadata: Metadata,
86 nullable: bool,
87
88 /// The name of the definition being referred to.
89 ref_: String,
90 },
91
92 /// The [type](https://tools.ietf.org/html/rfc8927#section-2.2.3) form.
93 ///
94 /// The type form accepts JSON "primitives" (booleans, numbers, strings)
95 /// whose value fall within a certain "type". These types are enumerated in
96 /// [`Type`].
97 Type {
98 definitions: Definitions,
99 metadata: Metadata,
100 nullable: bool,
101
102 /// The type of primitive value accepted.
103 type_: Type,
104 },
105
106 /// The [enum](https://tools.ietf.org/html/rfc8927#section-2.2.4) form.
107 ///
108 /// The enum form accepts JSON strings whose values are within an enumerated
109 /// set.
110 Enum {
111 definitions: Definitions,
112 metadata: Metadata,
113 nullable: bool,
114
115 /// The values the schema accepts.
116 enum_: BTreeSet<String>,
117 },
118
119 /// The [elements](https://tools.ietf.org/html/rfc8927#section-2.2.5) form.
120 ///
121 /// The elements form accepts JSON arrays, and each element of the array is
122 /// validated against a sub-schema.
123 Elements {
124 definitions: Definitions,
125 metadata: Metadata,
126 nullable: bool,
127
128 /// A schema for the elements of the array.
129 elements: Box<Schema>,
130 },
131
132 /// The [properties](https://tools.ietf.org/html/rfc8927#section-2.2.6)
133 /// form.
134 ///
135 /// The properties form accepts JSON objects being used as "structs".
136 Properties {
137 definitions: Definitions,
138 metadata: Metadata,
139 nullable: bool,
140
141 /// The required properties of the "struct", and the schema that each
142 /// must satisfy.
143 properties: BTreeMap<String, Schema>,
144
145 /// The optional properties of the "struct", and the schema that each
146 /// must satisfy if present.
147 optional_properties: BTreeMap<String, Schema>,
148
149 /// Whether the `properties` keyword is present on the schema.
150 ///
151 /// It is invalid to set this to `false` while having `properties` be
152 /// non-empty.
153 ///
154 /// This is used only to handle the corner case of a properties-form
155 /// schema being used to validate a non-object; in order to ensure the
156 /// returned `schema_path` points to a part of the schema that really
157 /// exists, validators need to be able to tell the difference between
158 /// `properties` being an empty object versus being omitted from the
159 /// schema.
160 ///
161 /// This field does not affect whether an input is valid. It only
162 /// affects the `schema_path` that will be returned if that input is not
163 /// an object. For more details, see the first sub-bullet after
164 /// "Otherwise" in [RFC 8927, Section
165 /// 3.3.6](https://tools.ietf.org/html/rfc8927#section-3.3.6).
166 ///
167 /// [`Schema::from_serde_schema`] correctly handles populating this
168 /// field. If you are constructing schemas by hand and want to play it
169 /// safe, it is always safe to set this to `true`.
170 properties_is_present: bool,
171
172 /// Whether additional properties not specified in `properties` or
173 /// `optional_properties` are permitted.
174 additional_properties: bool,
175 },
176
177 /// The [values](https://tools.ietf.org/html/rfc8927#section-2.2.7) form.
178 ///
179 /// The values form accepts JSON objects being used as "dictionaries"; each
180 /// value of the dictionary is validated against a sub-schema.
181 Values {
182 definitions: Definitions,
183 metadata: Metadata,
184 nullable: bool,
185
186 /// A schema for the values of the "dictionary" object.
187 values: Box<Schema>,
188 },
189
190 /// The [discriminator](https://tools.ietf.org/html/rfc8927#section-2.2.8)
191 /// form.
192 ///
193 /// The discriminator form accepts JSON objects being used as "discriminated
194 /// unions", or "tagged unions".
195 Discriminator {
196 definitions: Definitions,
197 metadata: Metadata,
198 nullable: bool,
199
200 /// The "discriminator" property of the schema.
201 ///
202 /// For an input to be valid, this property must exist and its value
203 /// must be a key in `mapping`.
204 discriminator: String,
205
206 /// A mapping from the value of the `discriminator` property in the
207 /// input to a schema that the rest of the input (without the
208 /// `discriminator` property) must satisfy.
209 mapping: BTreeMap<String, Schema>,
210 },
211}
212
213/// The values [`Schema::Type::type_`] may take on.
214#[derive(Clone, Debug, PartialEq, Eq)]
215pub enum Type {
216 /// Either JSON `true` or `false`.
217 Boolean,
218
219 /// A JSON number with zero fractional part within the range of [`i8`].
220 Int8,
221
222 /// A JSON number with zero fractional part within the range of [`u8`].
223 Uint8,
224
225 /// A JSON number with zero fractional part within the range of [`i16`].
226 Int16,
227
228 /// A JSON number with zero fractional part within the range of [`u16`].
229 Uint16,
230
231 /// A JSON number with zero fractional part within the range of [`i32`].
232 Int32,
233
234 /// A JSON number with zero fractional part within the range of [`u32`].
235 Uint32,
236
237 /// A JSON number. Code generators will treat this like a Rust [`f32`].
238 Float32,
239
240 /// A JSON number. Code generators will treat this like a Rust [`f64`].
241 Float64,
242
243 /// A JSON string.
244 String,
245
246 /// A JSON string encoding a [RFC3339](https://tools.ietf.org/html/rfc3339)
247 /// timestamp.
248 Timestamp,
249}
250
251/// Errors that may arise from [`Schema::from_serde_schema`].
252#[derive(Clone, Debug, PartialEq, Eq, Error)]
253pub enum FromSerdeSchemaError {
254 /// Indicates the schema uses an invalid combination of keywords.
255 ///
256 /// ```
257 /// use jtd::{FromSerdeSchemaError, Schema, SerdeSchema};
258 ///
259 /// assert_eq!(
260 /// Err(FromSerdeSchemaError::InvalidForm),
261 ///
262 /// // it's invalid to have both "type" and "enum" on a schema
263 /// Schema::from_serde_schema(SerdeSchema {
264 /// type_: Some("uint8".to_owned()),
265 /// enum_: Some(Default::default()),
266 /// ..Default::default()
267 /// })
268 /// )
269 /// ```
270 #[error("invalid combination of keywords in schema")]
271 InvalidForm,
272
273 /// Indicates the schema uses a value for `type` that isn't in [`Type`].
274 ///
275 /// ```
276 /// use jtd::{FromSerdeSchemaError, Schema, SerdeSchema};
277 ///
278 /// assert_eq!(
279 /// Err(FromSerdeSchemaError::InvalidType("uint64".to_owned())),
280 ///
281 /// // there is no uint64 in JSON Typedef
282 /// Schema::from_serde_schema(SerdeSchema {
283 /// type_: Some("uint64".to_owned()),
284 /// ..Default::default()
285 /// })
286 /// )
287 /// ```
288 #[error("invalid type: {0:?}")]
289 InvalidType(String),
290
291 /// Indicates the schema has the same value appearing twice in an `enum`.
292 ///
293 /// ```
294 /// use jtd::{FromSerdeSchemaError, Schema, SerdeSchema};
295 ///
296 /// assert_eq!(
297 /// Err(FromSerdeSchemaError::DuplicatedEnumValue("foo".to_owned())),
298 ///
299 /// // it's invalid to have the same value appear twice in an enum array
300 /// Schema::from_serde_schema(SerdeSchema {
301 /// enum_: Some(vec!["foo".into(), "bar".into(), "foo".into()]),
302 /// ..Default::default()
303 /// })
304 /// )
305 /// ```
306 #[error("duplicated enum value: {0:?}")]
307 DuplicatedEnumValue(String),
308}
309
310/// Errors that may arise from [`Schema::validate`].
311#[derive(Clone, Debug, PartialEq, Eq, Error)]
312pub enum SchemaValidateError {
313 /// Indicates the schema has a `ref` to a definition that doesn't exist.
314 ///
315 /// ```
316 /// use jtd::{Schema, SchemaValidateError};
317 ///
318 /// assert_eq!(
319 /// Err(SchemaValidateError::NoSuchDefinition("foo".into())),
320 ///
321 /// // a "ref" without definitions is always invalid
322 /// Schema::Ref {
323 /// definitions: Default::default(),
324 /// metadata: Default::default(),
325 /// nullable: Default::default(),
326 /// ref_: "foo".into(),
327 /// }.validate(),
328 /// )
329 /// ```
330 #[error("no such definition: {0:?}")]
331 NoSuchDefinition(String),
332
333 /// Indicates the schema has non-empty `definitions` below the root level.
334 ///
335 /// ```
336 /// use jtd::{Schema, SchemaValidateError};
337 ///
338 /// assert_eq!(
339 /// Err(SchemaValidateError::NonRootDefinitions),
340 ///
341 /// // definitions can only be present at the root level
342 /// Schema::Elements {
343 /// definitions: Default::default(),
344 /// metadata: Default::default(),
345 /// nullable: Default::default(),
346 /// elements: Box::new(Schema::Empty {
347 /// definitions: vec![(
348 /// "foo".to_owned(),
349 /// Schema::Empty {
350 /// definitions: Default::default(),
351 /// metadata: Default::default(),
352 /// }
353 /// )].into_iter().collect(),
354 /// metadata: Default::default(),
355 /// }),
356 /// }.validate(),
357 /// )
358 /// ```
359 #[error("non-root definitions")]
360 NonRootDefinitions,
361
362 /// Indicates the schema has an `enum` with no values in it.
363 ///
364 /// ```
365 /// use jtd::{Schema, SchemaValidateError};
366 ///
367 /// assert_eq!(
368 /// Err(SchemaValidateError::EmptyEnum),
369 ///
370 /// // empty enums are illegal
371 /// Schema::Enum {
372 /// definitions: Default::default(),
373 /// metadata: Default::default(),
374 /// nullable: Default::default(),
375 /// enum_: Default::default(),
376 /// }.validate(),
377 /// )
378 /// ```
379 #[error("empty enum")]
380 EmptyEnum,
381
382 /// Indicates the schema has the same property appear in `properties` and
383 /// `optional_properties`.
384 ///
385 /// ```
386 /// use jtd::{Schema, SchemaValidateError};
387 ///
388 /// assert_eq!(
389 /// Err(SchemaValidateError::RepeatedProperty("foo".into())),
390 ///
391 /// // properties and optional_properties must not overlap
392 /// Schema::Properties {
393 /// definitions: Default::default(),
394 /// metadata: Default::default(),
395 /// nullable: Default::default(),
396 /// properties: vec![(
397 /// "foo".to_owned(),
398 /// Schema::Empty {
399 /// definitions: Default::default(),
400 /// metadata: Default::default(),
401 /// },
402 /// )].into_iter().collect(),
403 /// optional_properties: vec![(
404 /// "foo".to_owned(),
405 /// Schema::Empty {
406 /// definitions: Default::default(),
407 /// metadata: Default::default(),
408 /// },
409 /// )].into_iter().collect(),
410 /// properties_is_present: true,
411 /// additional_properties: false,
412 /// }.validate(),
413 /// )
414 /// ```
415 #[error("property repeated in optionalProperties: {0:?}")]
416 RepeatedProperty(String),
417
418 /// Indicates the schema has a value in `mapping` with `nullable` set to
419 /// `true`.
420 ///
421 /// ```
422 /// use jtd::{Schema, SchemaValidateError};
423 ///
424 /// assert_eq!(
425 /// Err(SchemaValidateError::NullableMapping),
426 ///
427 /// // mappings must not be nullable
428 /// Schema::Discriminator {
429 /// definitions: Default::default(),
430 /// metadata: Default::default(),
431 /// nullable: Default::default(),
432 /// discriminator: "foo".into(),
433 /// mapping: vec![(
434 /// "bar".to_owned(),
435 /// Schema::Properties {
436 /// definitions: Default::default(),
437 /// metadata: Default::default(),
438 /// nullable: true,
439 /// properties: Default::default(),
440 /// optional_properties: Default::default(),
441 /// properties_is_present: true,
442 /// additional_properties: false,
443 /// }
444 /// )].into_iter().collect(),
445 /// }.validate(),
446 /// );
447 /// ```
448 #[error("nullable schema in mapping")]
449 NullableMapping,
450
451 /// Indicates the schema has a value in `mapping` that isn't a
452 /// [`Schema::Properties`].
453 ///
454 /// ```
455 /// use jtd::{Schema, SchemaValidateError};
456 ///
457 /// assert_eq!(
458 /// Err(SchemaValidateError::NonPropertiesMapping),
459 ///
460 /// // mappings must be of the properties form
461 /// Schema::Discriminator {
462 /// definitions: Default::default(),
463 /// metadata: Default::default(),
464 /// nullable: Default::default(),
465 /// discriminator: "foo".into(),
466 /// mapping: vec![(
467 /// "bar".to_owned(),
468 /// Schema::Empty {
469 /// definitions: Default::default(),
470 /// metadata: Default::default(),
471 /// }
472 /// )].into_iter().collect(),
473 /// }.validate(),
474 /// );
475 /// ```
476 #[error("non-properties schema in mapping")]
477 NonPropertiesMapping,
478
479 /// Indicates the schema has a value in `mapping` whose `properties` or
480 /// `optional_properties` contains `discriminator`.
481 ///
482 /// ```
483 /// use jtd::{Schema, SchemaValidateError};
484 ///
485 /// assert_eq!(
486 /// Err(SchemaValidateError::RepeatedDiscriminator("foo".into())),
487 ///
488 /// // mappings must not re-define the discriminator property
489 /// Schema::Discriminator {
490 /// definitions: Default::default(),
491 /// metadata: Default::default(),
492 /// nullable: Default::default(),
493 /// discriminator: "foo".into(),
494 /// mapping: vec![(
495 /// "bar".to_owned(),
496 /// Schema::Properties {
497 /// definitions: Default::default(),
498 /// metadata: Default::default(),
499 /// nullable: Default::default(),
500 /// properties: vec![(
501 /// "foo".into(),
502 /// Schema::Empty {
503 /// definitions: Default::default(),
504 /// metadata: Default::default(),
505 /// }
506 /// )].into_iter().collect(),
507 /// optional_properties: Default::default(),
508 /// properties_is_present: true,
509 /// additional_properties: false,
510 /// }
511 /// )].into_iter().collect(),
512 /// }.validate(),
513 /// );
514 /// ```
515 #[error("discriminator redefined in mapping: {0:?}")]
516 RepeatedDiscriminator(String),
517}
518
519// Index of valid form "signatures" -- i.e., combinations of the presence of the
520// keywords (in order):
521//
522// ref type enum elements properties optionalProperties additionalProperties
523// values discriminator mapping
524//
525// The keywords "definitions", "nullable", and "metadata" are not included here,
526// because they would restrict nothing.
527const VALID_FORM_SIGNATURES: [[bool; 10]; 13] = [
528 // Empty form
529 [
530 false, false, false, false, false, false, false, false, false, false,
531 ],
532 // Ref form
533 [
534 true, false, false, false, false, false, false, false, false, false,
535 ],
536 // Type form
537 [
538 false, true, false, false, false, false, false, false, false, false,
539 ],
540 // Enum form
541 [
542 false, false, true, false, false, false, false, false, false, false,
543 ],
544 // Elements form
545 [
546 false, false, false, true, false, false, false, false, false, false,
547 ],
548 // Properties form -- properties or optional properties or both, and never
549 // additional properties on its own
550 [
551 false, false, false, false, true, false, false, false, false, false,
552 ],
553 [
554 false, false, false, false, false, true, false, false, false, false,
555 ],
556 [
557 false, false, false, false, true, true, false, false, false, false,
558 ],
559 [
560 false, false, false, false, true, false, true, false, false, false,
561 ],
562 [
563 false, false, false, false, false, true, true, false, false, false,
564 ],
565 [
566 false, false, false, false, true, true, true, false, false, false,
567 ],
568 // Values form
569 [
570 false, false, false, false, false, false, false, true, false, false,
571 ],
572 // Discriminator form
573 [
574 false, false, false, false, false, false, false, false, true, true,
575 ],
576];
577
578impl Schema {
579 /// Converts a [`Schema`] into a [`SerdeSchema`].
580 ///
581 /// ```
582 /// use jtd::{Schema, SerdeSchema, Type};
583 ///
584 /// assert_eq!(
585 /// SerdeSchema {
586 /// type_: Some("uint8".to_owned()),
587 /// ..Default::default()
588 /// },
589 /// Schema::Type {
590 /// definitions: Default::default(),
591 /// metadata: Default::default(),
592 /// nullable: false,
593 /// type_: Type::Uint8,
594 /// }.into_serde_schema(),
595 /// );
596 /// ```
597 pub fn into_serde_schema(self) -> SerdeSchema {
598 let mut serde_schema: SerdeSchema = Default::default();
599
600 match self {
601 Schema::Empty {
602 definitions,
603 metadata,
604 } => {
605 serde_schema.definitions = Self::definitions_into_serde_schema(definitions);
606 serde_schema.metadata = Self::metadata_into_serde_schema(metadata);
607 }
608
609 Schema::Ref {
610 definitions,
611 metadata,
612 nullable,
613 ref_,
614 } => {
615 serde_schema.definitions = Self::definitions_into_serde_schema(definitions);
616 serde_schema.metadata = Self::metadata_into_serde_schema(metadata);
617 serde_schema.nullable = Self::nullable_into_serde_schema(nullable);
618 serde_schema.ref_ = Some(ref_);
619 }
620
621 Schema::Type {
622 definitions,
623 metadata,
624 nullable,
625 type_,
626 } => {
627 serde_schema.definitions = Self::definitions_into_serde_schema(definitions);
628 serde_schema.metadata = Self::metadata_into_serde_schema(metadata);
629 serde_schema.nullable = Self::nullable_into_serde_schema(nullable);
630 serde_schema.type_ = Some(
631 match type_ {
632 Type::Boolean => "boolean",
633 Type::Int8 => "int8",
634 Type::Uint8 => "uint8",
635 Type::Int16 => "int16",
636 Type::Uint16 => "uint16",
637 Type::Int32 => "int32",
638 Type::Uint32 => "uint32",
639 Type::Float32 => "float32",
640 Type::Float64 => "float64",
641 Type::String => "string",
642 Type::Timestamp => "timestamp",
643 }
644 .to_owned(),
645 );
646 }
647
648 Schema::Enum {
649 definitions,
650 metadata,
651 nullable,
652 enum_,
653 } => {
654 serde_schema.definitions = Self::definitions_into_serde_schema(definitions);
655 serde_schema.metadata = Self::metadata_into_serde_schema(metadata);
656 serde_schema.nullable = Self::nullable_into_serde_schema(nullable);
657 serde_schema.enum_ = Some(enum_.into_iter().collect());
658 }
659
660 Schema::Elements {
661 definitions,
662 metadata,
663 nullable,
664 elements,
665 } => {
666 serde_schema.definitions = Self::definitions_into_serde_schema(definitions);
667 serde_schema.metadata = Self::metadata_into_serde_schema(metadata);
668 serde_schema.nullable = Self::nullable_into_serde_schema(nullable);
669 serde_schema.elements = Some(Box::new(elements.into_serde_schema()));
670 }
671
672 Schema::Properties {
673 definitions,
674 metadata,
675 nullable,
676 properties,
677 optional_properties,
678 properties_is_present,
679 additional_properties,
680 } => {
681 serde_schema.definitions = Self::definitions_into_serde_schema(definitions);
682 serde_schema.metadata = Self::metadata_into_serde_schema(metadata);
683 serde_schema.nullable = Self::nullable_into_serde_schema(nullable);
684
685 if properties_is_present {
686 serde_schema.properties = Some(
687 properties
688 .into_iter()
689 .map(|(k, v)| (k, v.into_serde_schema()))
690 .collect(),
691 );
692 }
693
694 if !optional_properties.is_empty() {
695 serde_schema.optional_properties = Some(
696 optional_properties
697 .into_iter()
698 .map(|(k, v)| (k, v.into_serde_schema()))
699 .collect(),
700 );
701 }
702
703 if additional_properties {
704 serde_schema.additional_properties = Some(additional_properties);
705 }
706 }
707
708 Schema::Values {
709 definitions,
710 metadata,
711 nullable,
712 values,
713 } => {
714 serde_schema.definitions = Self::definitions_into_serde_schema(definitions);
715 serde_schema.metadata = Self::metadata_into_serde_schema(metadata);
716 serde_schema.nullable = Self::nullable_into_serde_schema(nullable);
717 serde_schema.values = Some(Box::new(values.into_serde_schema()));
718 }
719
720 Schema::Discriminator {
721 definitions,
722 metadata,
723 nullable,
724 discriminator,
725 mapping,
726 } => {
727 serde_schema.definitions = Self::definitions_into_serde_schema(definitions);
728 serde_schema.metadata = Self::metadata_into_serde_schema(metadata);
729 serde_schema.nullable = Self::nullable_into_serde_schema(nullable);
730 serde_schema.discriminator = Some(discriminator);
731 serde_schema.mapping = Some(
732 mapping
733 .into_iter()
734 .map(|(k, v)| (k, v.into_serde_schema()))
735 .collect(),
736 );
737 }
738 }
739
740 serde_schema
741 }
742
743 fn definitions_into_serde_schema(
744 definitions: Definitions,
745 ) -> Option<BTreeMap<String, SerdeSchema>> {
746 if definitions.is_empty() {
747 None
748 } else {
749 Some(
750 definitions
751 .into_iter()
752 .map(|(k, v)| (k, v.into_serde_schema()))
753 .collect(),
754 )
755 }
756 }
757
758 fn metadata_into_serde_schema(metadata: Metadata) -> Option<BTreeMap<String, Value>> {
759 if metadata.is_empty() {
760 None
761 } else {
762 Some(metadata)
763 }
764 }
765
766 fn nullable_into_serde_schema(nullable: bool) -> Option<bool> {
767 if nullable {
768 Some(true)
769 } else {
770 None
771 }
772 }
773
774 /// Constructs a [`Schema`] from a [`SerdeSchema`].
775 ///
776 /// ```
777 /// use jtd::{Schema, SerdeSchema, Type};
778 ///
779 /// assert_eq!(
780 /// Schema::Type {
781 /// definitions: Default::default(),
782 /// metadata: Default::default(),
783 /// nullable: false,
784 /// type_: Type::Uint8,
785 /// },
786 /// Schema::from_serde_schema(SerdeSchema {
787 /// type_: Some("uint8".to_owned()),
788 /// ..Default::default()
789 /// }).unwrap(),
790 /// );
791 /// ```
792 ///
793 /// See the documentation for [`FromSerdeSchemaError`] for examples of how
794 /// this function may return an error.
795 pub fn from_serde_schema(serde_schema: SerdeSchema) -> Result<Self, FromSerdeSchemaError> {
796 let mut definitions = BTreeMap::new();
797 for (name, sub_schema) in serde_schema.definitions.unwrap_or_default() {
798 definitions.insert(name, Self::from_serde_schema(sub_schema)?);
799 }
800
801 let metadata = serde_schema.metadata.unwrap_or_default();
802 let nullable = serde_schema.nullable.unwrap_or(false);
803
804 // Ensure the schema is using a valid combination of keywords.
805 let form_signature = [
806 serde_schema.ref_.is_some(),
807 serde_schema.type_.is_some(),
808 serde_schema.enum_.is_some(),
809 serde_schema.elements.is_some(),
810 serde_schema.properties.is_some(),
811 serde_schema.optional_properties.is_some(),
812 serde_schema.additional_properties.is_some(),
813 serde_schema.values.is_some(),
814 serde_schema.discriminator.is_some(),
815 serde_schema.mapping.is_some(),
816 ];
817
818 if !VALID_FORM_SIGNATURES.contains(&form_signature) {
819 return Err(FromSerdeSchemaError::InvalidForm);
820 }
821
822 // From here on out, we can use the presence of certain keywords to
823 // determine the form the schema takes on.
824 //
825 // We'll handle the empty form as a fallback, and handle the other forms
826 // in standard order.
827 if let Some(ref_) = serde_schema.ref_ {
828 return Ok(Schema::Ref {
829 definitions,
830 metadata,
831 nullable,
832 ref_,
833 });
834 }
835
836 if let Some(type_) = serde_schema.type_ {
837 let type_ = match &type_[..] {
838 "boolean" => Type::Boolean,
839 "int8" => Type::Int8,
840 "uint8" => Type::Uint8,
841 "int16" => Type::Int16,
842 "uint16" => Type::Uint16,
843 "int32" => Type::Int32,
844 "uint32" => Type::Uint32,
845 "float32" => Type::Float32,
846 "float64" => Type::Float64,
847 "string" => Type::String,
848 "timestamp" => Type::Timestamp,
849 _ => return Err(FromSerdeSchemaError::InvalidType(type_)),
850 };
851
852 return Ok(Schema::Type {
853 definitions,
854 metadata,
855 nullable,
856 type_,
857 });
858 }
859
860 if let Some(enum_) = serde_schema.enum_ {
861 // We do this construction by hand, rather than using collect, to
862 // detect the case of an enum value being repeated. This can't be
863 // detected once the values are put in the set.
864 let mut values = BTreeSet::new();
865 for value in enum_ {
866 if values.contains(&value) {
867 return Err(FromSerdeSchemaError::DuplicatedEnumValue(value));
868 }
869
870 values.insert(value);
871 }
872
873 return Ok(Schema::Enum {
874 definitions,
875 metadata,
876 nullable,
877 enum_: values,
878 });
879 }
880
881 if let Some(elements) = serde_schema.elements {
882 return Ok(Schema::Elements {
883 definitions,
884 metadata,
885 nullable,
886 elements: Box::new(Self::from_serde_schema(*elements)?),
887 });
888 }
889
890 if serde_schema.properties.is_some() || serde_schema.optional_properties.is_some() {
891 let properties_is_present = serde_schema.properties.is_some();
892 let additional_properties = serde_schema.additional_properties.unwrap_or(false);
893
894 let mut properties = BTreeMap::new();
895 for (name, sub_schema) in serde_schema.properties.unwrap_or_default() {
896 properties.insert(name, Self::from_serde_schema(sub_schema)?);
897 }
898
899 let mut optional_properties = BTreeMap::new();
900 for (name, sub_schema) in serde_schema.optional_properties.unwrap_or_default() {
901 optional_properties.insert(name, Self::from_serde_schema(sub_schema)?);
902 }
903
904 return Ok(Schema::Properties {
905 definitions,
906 metadata,
907 nullable,
908 properties,
909 optional_properties,
910 properties_is_present,
911 additional_properties,
912 });
913 }
914
915 if let Some(values) = serde_schema.values {
916 return Ok(Schema::Values {
917 definitions,
918 metadata,
919 nullable,
920 values: Box::new(Self::from_serde_schema(*values)?),
921 });
922 }
923
924 if let Some(discriminator) = serde_schema.discriminator {
925 // This is safe because the form signature check ensures mapping is
926 // present if discriminator is present.
927 let mut mapping = BTreeMap::new();
928 for (name, sub_schema) in serde_schema.mapping.unwrap() {
929 mapping.insert(name, Self::from_serde_schema(sub_schema)?);
930 }
931
932 return Ok(Schema::Discriminator {
933 definitions,
934 metadata,
935 nullable,
936 discriminator,
937 mapping,
938 });
939 }
940
941 Ok(Schema::Empty {
942 definitions,
943 metadata,
944 })
945 }
946
947 /// Ensures a [`Schema`] is well-formed.
948 ///
949 /// ```
950 /// use jtd::{Schema, Type};
951 ///
952 /// let schema = Schema::Type {
953 /// definitions: Default::default(),
954 /// metadata: Default::default(),
955 /// nullable: false,
956 /// type_: Type::Uint8,
957 /// };
958 ///
959 /// schema.validate().expect("Invalid schema");
960 /// ```
961 ///
962 /// See the documentation for [`SchemaValidateError`] for examples of how
963 /// this function may return an error.
964 pub fn validate(&self) -> Result<(), SchemaValidateError> {
965 self._validate(None)
966 }
967
968 fn _validate(&self, root: Option<&Self>) -> Result<(), SchemaValidateError> {
969 let sub_root = root.or(Some(self));
970
971 if root.is_some() && !self.definitions().is_empty() {
972 return Err(SchemaValidateError::NonRootDefinitions);
973 }
974
975 for sub_schema in self.definitions().values() {
976 sub_schema._validate(sub_root)?;
977 }
978
979 match self {
980 Self::Empty { .. } => {}
981 Self::Ref { ref_, .. } => {
982 if !sub_root
983 .map(|r| r.definitions())
984 .unwrap()
985 .contains_key(ref_)
986 {
987 return Err(SchemaValidateError::NoSuchDefinition(ref_.clone()));
988 }
989 }
990 Self::Type { .. } => {}
991 Self::Enum { enum_, .. } => {
992 if enum_.is_empty() {
993 return Err(SchemaValidateError::EmptyEnum);
994 }
995 }
996 Self::Elements { elements, .. } => {
997 elements._validate(sub_root)?;
998 }
999 Self::Properties {
1000 properties,
1001 optional_properties,
1002 ..
1003 } => {
1004 for key in properties.keys() {
1005 if optional_properties.contains_key(key) {
1006 return Err(SchemaValidateError::RepeatedProperty(key.clone()));
1007 }
1008 }
1009
1010 for sub_schema in properties.values() {
1011 sub_schema._validate(sub_root)?;
1012 }
1013
1014 for sub_schema in optional_properties.values() {
1015 sub_schema._validate(sub_root)?;
1016 }
1017 }
1018 Self::Values { values, .. } => {
1019 values._validate(sub_root)?;
1020 }
1021 Self::Discriminator {
1022 discriminator,
1023 mapping,
1024 ..
1025 } => {
1026 for sub_schema in mapping.values() {
1027 if let Self::Properties {
1028 nullable,
1029 properties,
1030 optional_properties,
1031 ..
1032 } = sub_schema
1033 {
1034 if *nullable {
1035 return Err(SchemaValidateError::NullableMapping);
1036 }
1037
1038 if properties.contains_key(discriminator)
1039 || optional_properties.contains_key(discriminator)
1040 {
1041 return Err(SchemaValidateError::RepeatedDiscriminator(
1042 discriminator.clone(),
1043 ));
1044 }
1045 } else {
1046 return Err(SchemaValidateError::NonPropertiesMapping);
1047 }
1048
1049 sub_schema._validate(sub_root)?;
1050 }
1051 }
1052 }
1053
1054 Ok(())
1055 }
1056
1057 /// Gets the schema's definitions.
1058 ///
1059 /// ```
1060 /// use jtd::{Definitions, Schema};
1061 ///
1062 /// assert_eq!(
1063 /// &vec![(
1064 /// "foo".to_owned(),
1065 /// Schema::Empty {
1066 /// definitions: Default::default(),
1067 /// metadata: Default::default(),
1068 /// },
1069 /// )].into_iter().collect::<Definitions>(),
1070 ///
1071 /// Schema::Empty {
1072 /// definitions: vec![(
1073 /// "foo".to_owned(),
1074 /// Schema::Empty {
1075 /// definitions: Default::default(),
1076 /// metadata: Default::default(),
1077 /// },
1078 /// )].into_iter().collect(),
1079 /// metadata: Default::default(),
1080 /// }.definitions(),
1081 /// );
1082 /// ```
1083 pub fn definitions(&self) -> &BTreeMap<String, Schema> {
1084 match self {
1085 Self::Empty { definitions, .. } => definitions,
1086 Self::Ref { definitions, .. } => definitions,
1087 Self::Enum { definitions, .. } => definitions,
1088 Self::Type { definitions, .. } => definitions,
1089 Self::Elements { definitions, .. } => definitions,
1090 Self::Properties { definitions, .. } => definitions,
1091 Self::Values { definitions, .. } => definitions,
1092 Self::Discriminator { definitions, .. } => definitions,
1093 }
1094 }
1095
1096 /// Gets the schema's metadata.
1097 ///
1098 /// ```
1099 /// use jtd::{Metadata, Schema};
1100 /// use serde_json::json;
1101 ///
1102 /// assert_eq!(
1103 /// &vec![(
1104 /// "foo".to_owned(),
1105 /// json!("bar"),
1106 /// )].into_iter().collect::<Metadata>(),
1107 ///
1108 /// Schema::Empty {
1109 /// definitions: Default::default(),
1110 /// metadata: vec![(
1111 /// "foo".to_owned(),
1112 /// json!("bar"),
1113 /// )].into_iter().collect(),
1114 /// }.metadata(),
1115 /// );
1116 /// ```
1117 pub fn metadata(&self) -> &BTreeMap<String, Value> {
1118 match self {
1119 Self::Empty { metadata, .. } => metadata,
1120 Self::Ref { metadata, .. } => metadata,
1121 Self::Enum { metadata, .. } => metadata,
1122 Self::Type { metadata, .. } => metadata,
1123 Self::Elements { metadata, .. } => metadata,
1124 Self::Properties { metadata, .. } => metadata,
1125 Self::Values { metadata, .. } => metadata,
1126 Self::Discriminator { metadata, .. } => metadata,
1127 }
1128 }
1129
1130 /// Gets whether the schema is nullable.
1131 ///
1132 /// For [`Schema::Empty`], this always returns true. For all other forms,
1133 /// this fetches the `nullable` property.
1134 ///
1135 /// ```
1136 /// use jtd::{Schema, Type};
1137 ///
1138 /// assert!(
1139 /// Schema::Empty {
1140 /// definitions: Default::default(),
1141 /// metadata: Default::default(),
1142 /// }.nullable(),
1143 /// );
1144 ///
1145 /// assert!(
1146 /// !Schema::Type {
1147 /// definitions: Default::default(),
1148 /// metadata: Default::default(),
1149 /// nullable: false,
1150 /// type_: Type::Uint8,
1151 /// }.nullable(),
1152 /// );
1153 /// ```
1154 pub fn nullable(&self) -> bool {
1155 match self {
1156 Self::Empty { .. } => true,
1157 Self::Ref { nullable, .. } => *nullable,
1158 Self::Enum { nullable, .. } => *nullable,
1159 Self::Type { nullable, .. } => *nullable,
1160 Self::Elements { nullable, .. } => *nullable,
1161 Self::Properties { nullable, .. } => *nullable,
1162 Self::Values { nullable, .. } => *nullable,
1163 Self::Discriminator { nullable, .. } => *nullable,
1164 }
1165 }
1166}
1167
1168#[cfg(test)]
1169mod tests {
1170 use crate::{Schema, SerdeSchema};
1171
1172 #[test]
1173 fn invalid_schemas() {
1174 use std::collections::BTreeMap;
1175
1176 let test_cases: BTreeMap<String, serde_json::Value> = serde_json::from_str(include_str!(
1177 "../json-typedef-spec/tests/invalid_schemas.json"
1178 ))
1179 .expect("parse invalid_schemas.json");
1180
1181 for (test_case_name, test_case) in test_cases {
1182 if let Ok(serde_schema) = serde_json::from_value::<SerdeSchema>(test_case) {
1183 if let Ok(schema) = Schema::from_serde_schema(serde_schema) {
1184 if schema.validate().is_ok() {
1185 panic!(
1186 "failed to detect invalid schema: {}, got: {:?}",
1187 test_case_name, schema
1188 );
1189 }
1190 }
1191 }
1192 }
1193 }
1194
1195 #[test]
1196 fn valid_schemas() {
1197 use std::collections::BTreeMap;
1198
1199 #[derive(serde::Deserialize)]
1200 struct TestCase {
1201 schema: serde_json::Value,
1202 }
1203
1204 let test_cases: BTreeMap<String, TestCase> =
1205 serde_json::from_str(include_str!("../json-typedef-spec/tests/validation.json"))
1206 .expect("parse validation.json");
1207
1208 for (test_case_name, test_case) in test_cases {
1209 let serde_schema =
1210 serde_json::from_value::<SerdeSchema>(test_case.schema).expect(&test_case_name);
1211 let schema = Schema::from_serde_schema(serde_schema).expect(&test_case_name);
1212 schema.validate().expect(&test_case_name);
1213 }
1214 }
1215}