1use std::collections::{BTreeMap, BTreeSet};
56use std::num::{NonZeroU32, NonZeroU64};
57
58#[derive(Clone, Debug, Eq, PartialEq)]
65pub enum WireSchema {
66 Bool,
67 U8,
68 U16,
69 U32,
70 U64,
71 I8,
72 I16,
73 I32,
74 I64,
75 F32,
76 F64,
77 String,
78 Bytes,
81 Unit,
83 Freeform,
86 Option(Box<WireSchema>),
87 Seq(Box<WireSchema>),
88 Map {
89 key: Box<WireSchema>,
90 value: Box<WireSchema>,
91 },
92 Tuple(Vec<WireSchema>),
94 Array {
96 element: Box<WireSchema>,
97 length: usize,
98 },
99 Struct {
101 fields: Vec<WireField>,
102 },
103 Newtype(Box<WireSchema>),
106 Enum {
110 representation: EnumRepresentation,
111 variants: Vec<WireVariant>,
112 },
113 Opaque {
120 name: String,
121 wire: Box<WireSchema>,
122 },
123}
124
125#[derive(Clone, Debug, Eq, PartialEq)]
127pub struct WireField {
128 pub name: String,
130 pub schema: WireSchema,
131 pub presence: FieldPresence,
132}
133
134impl WireField {
135 #[must_use]
137 pub fn new(name: impl Into<String>, schema: WireSchema, presence: FieldPresence) -> Self {
138 Self {
139 name: name.into(),
140 schema,
141 presence,
142 }
143 }
144
145 #[must_use]
147 pub fn required(name: impl Into<String>, schema: WireSchema) -> Self {
148 Self::new(name, schema, FieldPresence::Required)
149 }
150}
151
152#[derive(Clone, Copy, Debug, Eq, PartialEq)]
158pub enum FieldPresence {
159 Required,
161 Defaulted,
164 Omissible,
167 Optional,
169}
170
171impl FieldPresence {
172 #[must_use]
174 pub const fn new(defaulted: bool, omissible: bool) -> Self {
175 match (defaulted, omissible) {
176 (false, false) => Self::Required,
177 (true, false) => Self::Defaulted,
178 (false, true) => Self::Omissible,
179 (true, true) => Self::Optional,
180 }
181 }
182
183 #[must_use]
185 pub const fn admits_absence(self) -> bool {
186 matches!(self, Self::Defaulted | Self::Optional)
187 }
188
189 const fn token(self) -> &'static str {
190 match self {
191 Self::Required => "required",
192 Self::Defaulted => "defaulted",
193 Self::Omissible => "omissible",
194 Self::Optional => "optional",
195 }
196 }
197}
198
199#[derive(Clone, Debug, Eq, PartialEq)]
201pub enum EnumRepresentation {
202 ExternallyTagged,
205 InternallyTagged { tag: String },
207 Untagged,
209}
210
211#[derive(Clone, Debug, Eq, PartialEq)]
213pub struct WireVariant {
214 pub name: String,
216 pub body: VariantBody,
217}
218
219impl WireVariant {
220 #[must_use]
222 pub fn new(name: impl Into<String>, body: VariantBody) -> Self {
223 Self {
224 name: name.into(),
225 body,
226 }
227 }
228}
229
230#[derive(Clone, Debug, Eq, PartialEq)]
232pub enum VariantBody {
233 Unit,
234 Other,
238 Newtype(Box<WireSchema>),
239 Tuple(Vec<WireSchema>),
240 Struct(Vec<WireField>),
241}
242
243impl VariantBody {
244 #[must_use]
246 pub fn newtype(inner: WireSchema) -> Self {
247 Self::Newtype(Box::new(inner))
248 }
249
250 #[must_use]
252 pub fn structure(fields: impl IntoIterator<Item = WireField>) -> Self {
253 Self::Struct(sorted_fields(fields))
254 }
255}
256
257fn sorted_fields(fields: impl IntoIterator<Item = WireField>) -> Vec<WireField> {
258 let mut fields = fields.into_iter().collect::<Vec<_>>();
259 fields.sort_by(|left, right| left.name.cmp(&right.name));
260 fields
261}
262
263impl WireSchema {
264 #[must_use]
266 pub fn option(inner: WireSchema) -> Self {
267 Self::Option(Box::new(inner))
268 }
269
270 #[must_use]
272 pub fn seq(item: WireSchema) -> Self {
273 Self::Seq(Box::new(item))
274 }
275
276 #[must_use]
278 pub fn map(key: WireSchema, value: WireSchema) -> Self {
279 Self::Map {
280 key: Box::new(key),
281 value: Box::new(value),
282 }
283 }
284
285 #[must_use]
287 pub fn array(element: WireSchema, length: usize) -> Self {
288 Self::Array {
289 element: Box::new(element),
290 length,
291 }
292 }
293
294 #[must_use]
296 pub fn newtype(inner: WireSchema) -> Self {
297 Self::Newtype(Box::new(inner))
298 }
299
300 #[must_use]
302 pub fn structure(fields: impl IntoIterator<Item = WireField>) -> Self {
303 Self::Struct {
304 fields: sorted_fields(fields),
305 }
306 }
307
308 #[must_use]
317 pub fn enumeration(
318 representation: EnumRepresentation,
319 variants: impl IntoIterator<Item = WireVariant>,
320 ) -> Self {
321 let mut variants = variants.into_iter().collect::<Vec<_>>();
322 if !matches!(representation, EnumRepresentation::Untagged) {
323 variants.sort_by(|left, right| left.name.cmp(&right.name));
324 }
325 Self::Enum {
326 representation,
327 variants,
328 }
329 }
330
331 #[must_use]
333 pub fn opaque(name: impl Into<String>, wire: WireSchema) -> Self {
334 Self::Opaque {
335 name: name.into(),
336 wire: Box::new(wire),
337 }
338 }
339
340 #[must_use]
346 pub fn resolved(&self) -> &WireSchema {
347 match self {
348 Self::Opaque { wire, .. } => wire.resolved(),
349 Self::Newtype(inner) => inner.resolved(),
350 other => other,
351 }
352 }
353
354 #[must_use]
361 pub fn canonical_json(&self) -> String {
362 let mut out = String::new();
363 self.render(&mut out);
364 out
365 }
366
367 pub(crate) fn render(&self, out: &mut String) {
368 match self {
369 Self::Bool => out.push_str(r#"{"kind":"bool"}"#),
370 Self::U8 => out.push_str(r#"{"kind":"u8"}"#),
371 Self::U16 => out.push_str(r#"{"kind":"u16"}"#),
372 Self::U32 => out.push_str(r#"{"kind":"u32"}"#),
373 Self::U64 => out.push_str(r#"{"kind":"u64"}"#),
374 Self::I8 => out.push_str(r#"{"kind":"i8"}"#),
375 Self::I16 => out.push_str(r#"{"kind":"i16"}"#),
376 Self::I32 => out.push_str(r#"{"kind":"i32"}"#),
377 Self::I64 => out.push_str(r#"{"kind":"i64"}"#),
378 Self::F32 => out.push_str(r#"{"kind":"f32"}"#),
379 Self::F64 => out.push_str(r#"{"kind":"f64"}"#),
380 Self::String => out.push_str(r#"{"kind":"string"}"#),
381 Self::Bytes => out.push_str(r#"{"kind":"bytes"}"#),
382 Self::Unit => out.push_str(r#"{"kind":"unit"}"#),
383 Self::Freeform => out.push_str(r#"{"kind":"freeform"}"#),
384 Self::Option(inner) => {
385 out.push_str(r#"{"kind":"option","value":"#);
386 inner.render(out);
387 out.push('}');
388 }
389 Self::Seq(item) => {
390 out.push_str(r#"{"item":"#);
391 item.render(out);
392 out.push_str(r#","kind":"seq"}"#);
393 }
394 Self::Map { key, value } => {
395 out.push_str(r#"{"key":"#);
396 key.render(out);
397 out.push_str(r#","kind":"map","value":"#);
398 value.render(out);
399 out.push('}');
400 }
401 Self::Tuple(items) => {
402 out.push_str(r#"{"items":"#);
403 render_list(items, out, WireSchema::render);
404 out.push_str(r#","kind":"tuple"}"#);
405 }
406 Self::Array { element, length } => {
407 out.push_str(r#"{"element":"#);
408 element.render(out);
409 out.push_str(r#","kind":"array","length":"#);
410 out.push_str(&length.to_string());
411 out.push('}');
412 }
413 Self::Struct { fields } => {
414 out.push_str(r#"{"fields":"#);
415 render_list(fields, out, WireField::render);
416 out.push_str(r#","kind":"struct"}"#);
417 }
418 Self::Newtype(inner) => {
419 out.push_str(r#"{"inner":"#);
420 inner.render(out);
421 out.push_str(r#","kind":"newtype"}"#);
422 }
423 Self::Enum {
424 representation,
425 variants,
426 } => {
427 out.push_str(r#"{"kind":"enum","representation":"#);
428 representation.render(out);
429 out.push_str(r#","variants":"#);
430 render_list(variants, out, WireVariant::render);
431 out.push('}');
432 }
433 Self::Opaque { name, wire } => {
434 out.push_str(r#"{"kind":"opaque","name":"#);
435 render_string(name, out);
436 out.push_str(r#","wire":"#);
437 wire.render(out);
438 out.push('}');
439 }
440 }
441 }
442}
443
444impl WireField {
445 fn render(&self, out: &mut String) {
446 out.push_str(r#"{"name":"#);
447 render_string(&self.name, out);
448 out.push_str(r#","presence":""#);
449 out.push_str(self.presence.token());
450 out.push_str(r#"","schema":"#);
451 self.schema.render(out);
452 out.push('}');
453 }
454}
455
456impl EnumRepresentation {
457 fn render(&self, out: &mut String) {
458 match self {
459 Self::ExternallyTagged => out.push_str(r#"{"style":"external"}"#),
460 Self::InternallyTagged { tag } => {
461 out.push_str(r#"{"style":"internal","tag":"#);
462 render_string(tag, out);
463 out.push('}');
464 }
465 Self::Untagged => out.push_str(r#"{"style":"untagged"}"#),
466 }
467 }
468}
469
470impl WireVariant {
471 fn render(&self, out: &mut String) {
472 out.push_str(r#"{"body":"#);
473 self.body.render(out);
474 out.push_str(r#","name":"#);
475 render_string(&self.name, out);
476 out.push('}');
477 }
478}
479
480impl VariantBody {
481 fn render(&self, out: &mut String) {
482 match self {
483 Self::Unit => out.push_str(r#"{"kind":"unit"}"#),
484 Self::Other => out.push_str(r#"{"kind":"other"}"#),
485 Self::Newtype(inner) => {
486 out.push_str(r#"{"inner":"#);
487 inner.render(out);
488 out.push_str(r#","kind":"newtype"}"#);
489 }
490 Self::Tuple(items) => {
491 out.push_str(r#"{"items":"#);
492 render_list(items, out, WireSchema::render);
493 out.push_str(r#","kind":"tuple"}"#);
494 }
495 Self::Struct(fields) => {
496 out.push_str(r#"{"fields":"#);
497 render_list(fields, out, WireField::render);
498 out.push_str(r#","kind":"struct"}"#);
499 }
500 }
501 }
502}
503
504pub(crate) fn render_list<T>(items: &[T], out: &mut String, render: impl Fn(&T, &mut String)) {
505 out.push('[');
506 for (index, item) in items.iter().enumerate() {
507 if index > 0 {
508 out.push(',');
509 }
510 render(item, out);
511 }
512 out.push(']');
513}
514
515pub(crate) fn render_string(value: &str, out: &mut String) {
517 out.push('"');
518 for character in value.chars() {
519 match character {
520 '"' => out.push_str("\\\""),
521 '\\' => out.push_str("\\\\"),
522 '\n' => out.push_str("\\n"),
523 '\r' => out.push_str("\\r"),
524 '\t' => out.push_str("\\t"),
525 control if control <= '\u{1f}' => {
526 out.push_str(&format!("\\u{:04x}", control as u32));
527 }
528 other => out.push(other),
529 }
530 }
531 out.push('"');
532}
533
534pub trait DescribeWire {
543 fn wire_schema() -> WireSchema;
545}
546
547macro_rules! primitive_wire_schema {
548 ($($ty:ty => $schema:expr),* $(,)?) => {
549 $(
550 impl DescribeWire for $ty {
551 fn wire_schema() -> WireSchema {
552 $schema
553 }
554 }
555 )*
556 };
557}
558
559primitive_wire_schema! {
560 bool => WireSchema::Bool,
561 u8 => WireSchema::U8,
562 u16 => WireSchema::U16,
563 u32 => WireSchema::U32,
564 u64 => WireSchema::U64,
565 usize => WireSchema::U64,
568 i8 => WireSchema::I8,
569 i16 => WireSchema::I16,
570 i32 => WireSchema::I32,
571 i64 => WireSchema::I64,
572 isize => WireSchema::I64,
573 f32 => WireSchema::F32,
574 f64 => WireSchema::F64,
575 String => WireSchema::String,
576 str => WireSchema::String,
577 () => WireSchema::Unit,
578 NonZeroU32 => WireSchema::U32,
579 NonZeroU64 => WireSchema::U64,
580}
581
582impl<T: DescribeWire + ?Sized> DescribeWire for &T {
583 fn wire_schema() -> WireSchema {
584 T::wire_schema()
585 }
586}
587
588impl<T: DescribeWire + ?Sized> DescribeWire for Box<T> {
589 fn wire_schema() -> WireSchema {
590 T::wire_schema()
591 }
592}
593
594impl<T: DescribeWire> DescribeWire for Option<T> {
595 fn wire_schema() -> WireSchema {
596 WireSchema::option(T::wire_schema())
597 }
598}
599
600impl<T: DescribeWire> DescribeWire for Vec<T> {
601 fn wire_schema() -> WireSchema {
602 WireSchema::seq(T::wire_schema())
603 }
604}
605
606impl<T: DescribeWire> DescribeWire for [T] {
607 fn wire_schema() -> WireSchema {
608 WireSchema::seq(T::wire_schema())
609 }
610}
611
612impl<T: DescribeWire, const N: usize> DescribeWire for [T; N] {
613 fn wire_schema() -> WireSchema {
614 WireSchema::array(T::wire_schema(), N)
615 }
616}
617
618impl<T: DescribeWire> DescribeWire for BTreeSet<T> {
619 fn wire_schema() -> WireSchema {
620 WireSchema::seq(T::wire_schema())
621 }
622}
623
624impl<K: DescribeWire, V: DescribeWire> DescribeWire for BTreeMap<K, V> {
625 fn wire_schema() -> WireSchema {
626 WireSchema::map(K::wire_schema(), V::wire_schema())
627 }
628}
629
630macro_rules! tuple_wire_schema {
631 ($($name:ident),+) => {
632 impl<$($name: DescribeWire),+> DescribeWire for ($($name,)+) {
633 fn wire_schema() -> WireSchema {
634 WireSchema::Tuple(::std::vec![$($name::wire_schema()),+])
635 }
636 }
637 };
638}
639
640tuple_wire_schema!(A);
641tuple_wire_schema!(A, B);
642tuple_wire_schema!(A, B, C);
643tuple_wire_schema!(A, B, C, D);
644tuple_wire_schema!(A, B, C, D, E);
645tuple_wire_schema!(A, B, C, D, E, F);
646
647impl DescribeWire for serde_json::Value {
648 fn wire_schema() -> WireSchema {
651 WireSchema::opaque("serde_json::Value", WireSchema::Freeform)
652 }
653}
654
655#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
657#[error("{path}: expected {expected}, found {found}")]
658pub struct WireMismatch {
659 pub path: String,
661 pub expected: String,
662 pub found: String,
663}
664
665impl WireMismatch {
666 fn new(path: &str, expected: impl Into<String>, found: impl Into<String>) -> Self {
667 Self {
668 path: if path.is_empty() {
669 String::from("$")
670 } else {
671 path.to_string()
672 },
673 expected: expected.into(),
674 found: found.into(),
675 }
676 }
677}
678
679impl WireSchema {
680 pub fn conforms(&self, value: &serde_json::Value) -> Result<(), WireMismatch> {
693 self.check(value, "")
694 }
695
696 fn check(&self, value: &serde_json::Value, path: &str) -> Result<(), WireMismatch> {
697 use serde_json::Value;
698
699 let mismatch = |expected: &str| Err(WireMismatch::new(path, expected, describe(value)));
700 match self {
701 Self::Opaque { wire, .. } => wire.check(value, path),
702 Self::Newtype(inner) => inner.check(value, path),
703 Self::Freeform => Ok(()),
704 Self::Bool => {
705 if value.is_boolean() {
706 Ok(())
707 } else {
708 mismatch("bool")
709 }
710 }
711 Self::U8 => check_unsigned(value, path, u64::from(u8::MAX), "u8"),
712 Self::U16 => check_unsigned(value, path, u64::from(u16::MAX), "u16"),
713 Self::U32 => check_unsigned(value, path, u64::from(u32::MAX), "u32"),
714 Self::U64 => check_unsigned(value, path, u64::MAX, "u64"),
715 Self::I8 => check_signed(value, path, i64::from(i8::MIN), i64::from(i8::MAX), "i8"),
716 Self::I16 => check_signed(value, path, i64::from(i16::MIN), i64::from(i16::MAX), "i16"),
717 Self::I32 => check_signed(value, path, i64::from(i32::MIN), i64::from(i32::MAX), "i32"),
718 Self::I64 => check_signed(value, path, i64::MIN, i64::MAX, "i64"),
719 Self::F32 | Self::F64 => {
720 if value.is_number() {
721 Ok(())
722 } else {
723 mismatch("a number")
724 }
725 }
726 Self::String => {
727 if value.is_string() {
728 Ok(())
729 } else {
730 mismatch("a string")
731 }
732 }
733 Self::Bytes => match value {
734 Value::String(_) => Ok(()),
735 Value::Array(items) => {
736 for (index, item) in items.iter().enumerate() {
737 check_unsigned(
738 item,
739 &format!("{path}[{index}]"),
740 u64::from(u8::MAX),
741 "a byte",
742 )?;
743 }
744 Ok(())
745 }
746 _ => mismatch("a byte string"),
747 },
748 Self::Unit => {
749 if value.is_null() {
750 Ok(())
751 } else {
752 mismatch("null")
753 }
754 }
755 Self::Option(inner) => {
756 if value.is_null() {
757 Ok(())
758 } else {
759 inner.check(value, path)
760 }
761 }
762 Self::Seq(item) => match value {
763 Value::Array(items) => {
764 for (index, element) in items.iter().enumerate() {
765 item.check(element, &format!("{path}[{index}]"))?;
766 }
767 Ok(())
768 }
769 _ => mismatch("a sequence"),
770 },
771 Self::Array { element, length } => match value {
772 Value::Array(items) if items.len() == *length => {
773 for (index, item) in items.iter().enumerate() {
774 element.check(item, &format!("{path}[{index}]"))?;
775 }
776 Ok(())
777 }
778 _ => mismatch(&format!("a sequence of exactly {length}")),
779 },
780 Self::Tuple(items) => match value {
781 Value::Array(elements) if elements.len() == items.len() => {
782 for (index, (schema, element)) in items.iter().zip(elements).enumerate() {
783 schema.check(element, &format!("{path}[{index}]"))?;
784 }
785 Ok(())
786 }
787 _ => mismatch(&format!("a tuple of {}", items.len())),
788 },
789 Self::Map { key, value: item } => match value {
790 Value::Object(entries) => {
791 for (name, entry) in entries {
792 key.check_key(name, path)?;
793 item.check(entry, &format!("{path}.{name}"))?;
794 }
795 Ok(())
796 }
797 _ => mismatch("a map"),
798 },
799 Self::Struct { fields } => match value {
800 Value::Object(entries) => check_fields(fields, entries, path),
801 _ => mismatch("a map"),
802 },
803 Self::Enum {
804 representation,
805 variants,
806 } => representation.check(variants, value, path),
807 }
808 }
809
810 fn check_key(&self, key: &str, path: &str) -> Result<(), WireMismatch> {
813 let path = format!("{path}.{key}(key)");
814 match self.resolved() {
815 Self::U8 | Self::U16 | Self::U32 | Self::U64 => match key.parse::<u64>() {
816 Ok(parsed) => self.check(&serde_json::Value::from(parsed), &path),
817 Err(_) => Err(WireMismatch::new(&path, "an unsigned key", "text")),
818 },
819 Self::I8 | Self::I16 | Self::I32 | Self::I64 => match key.parse::<i64>() {
820 Ok(parsed) => self.check(&serde_json::Value::from(parsed), &path),
821 Err(_) => Err(WireMismatch::new(&path, "a signed key", "text")),
822 },
823 _ => self.check(&serde_json::Value::String(key.to_string()), &path),
824 }
825 }
826}
827
828fn check_fields(
829 fields: &[WireField],
830 entries: &serde_json::Map<String, serde_json::Value>,
831 path: &str,
832) -> Result<(), WireMismatch> {
833 for field in fields {
834 match entries.get(&field.name) {
835 Some(value) => field
836 .schema
837 .check(value, &format!("{path}.{}", field.name))?,
838 None if field.presence.admits_absence() => {}
839 None => {
840 return Err(WireMismatch::new(
841 &format!("{path}.{}", field.name),
842 "the declared field",
843 "an absent field",
844 ));
845 }
846 }
847 }
848 for name in entries.keys() {
849 if !fields.iter().any(|field| field.name == *name) {
850 return Err(WireMismatch::new(
851 &format!("{path}.{name}"),
852 "no such field in the declared schema",
853 "a serialized field",
854 ));
855 }
856 }
857 Ok(())
858}
859
860impl EnumRepresentation {
861 fn check(
862 &self,
863 variants: &[WireVariant],
864 value: &serde_json::Value,
865 path: &str,
866 ) -> Result<(), WireMismatch> {
867 use serde_json::Value;
868
869 match self {
870 Self::ExternallyTagged => match value {
871 Value::String(name) => match find(variants, name) {
872 Some(variant)
873 if matches!(variant.body, VariantBody::Unit | VariantBody::Other) =>
874 {
875 Ok(())
876 }
877 Some(_) => Err(WireMismatch::new(path, "a unit variant name", "a string")),
878 None => Err(WireMismatch::new(path, "a declared variant", "a string")),
879 },
880 Value::Object(entries) if entries.len() == 1 => {
881 let Some((name, body)) = entries.iter().next() else {
882 return Err(WireMismatch::new(path, "a one-key map", "an empty map"));
883 };
884 let Some(variant) = find(variants, name) else {
885 return Err(WireMismatch::new(
886 &format!("{path}.{name}"),
887 "a declared variant",
888 "an unknown variant",
889 ));
890 };
891 variant.body.check(body, &format!("{path}.{name}"))
892 }
893 _ => Err(WireMismatch::new(
894 path,
895 "an externally tagged enum",
896 describe(value),
897 )),
898 },
899 Self::InternallyTagged { tag } => {
900 let Value::Object(entries) = value else {
901 return Err(WireMismatch::new(path, "a map", describe(value)));
902 };
903 let Some(Value::String(name)) = entries.get(tag) else {
904 return Err(WireMismatch::new(
905 &format!("{path}.{tag}"),
906 "the variant tag",
907 "an absent or non-text tag",
908 ));
909 };
910 let Some(variant) = find(variants, name) else {
911 return Err(WireMismatch::new(
912 &format!("{path}.{tag}"),
913 "a declared variant",
914 "an unknown variant",
915 ));
916 };
917 let mut body = entries.clone();
918 body.remove(tag);
919 match &variant.body {
920 VariantBody::Unit | VariantBody::Other => {
921 if body.is_empty() {
922 Ok(())
923 } else {
924 Err(WireMismatch::new(path, "no further fields", "extra fields"))
925 }
926 }
927 VariantBody::Struct(fields) => check_fields(fields, &body, path),
928 VariantBody::Newtype(inner) => match inner.resolved() {
931 WireSchema::Struct { fields } => check_fields(fields, &body, path),
932 _ => Err(WireMismatch::new(
933 path,
934 "a newtype variant over a map",
935 "a non-map inner shape",
936 )),
937 },
938 VariantBody::Tuple(_) => Err(WireMismatch::new(
939 path,
940 "no tuple variant under an internal tag",
941 "a tuple variant",
942 )),
943 }
944 }
945 Self::Untagged => {
946 for variant in variants {
947 if variant.body.check(value, path).is_ok() {
948 return Ok(());
949 }
950 }
951 Err(WireMismatch::new(
952 path,
953 "any declared untagged variant",
954 describe(value),
955 ))
956 }
957 }
958 }
959}
960
961fn find<'a>(variants: &'a [WireVariant], name: &str) -> Option<&'a WireVariant> {
962 variants.iter().find(|variant| variant.name == name)
963}
964
965impl VariantBody {
966 fn check(&self, value: &serde_json::Value, path: &str) -> Result<(), WireMismatch> {
967 match self {
968 Self::Unit | Self::Other => WireSchema::Unit.check(value, path),
969 Self::Newtype(inner) => inner.check(value, path),
970 Self::Tuple(items) => WireSchema::Tuple(items.clone()).check(value, path),
971 Self::Struct(fields) => match value {
972 serde_json::Value::Object(entries) => check_fields(fields, entries, path),
973 _ => Err(WireMismatch::new(path, "a map", describe(value))),
974 },
975 }
976 }
977}
978
979fn check_unsigned(
980 value: &serde_json::Value,
981 path: &str,
982 max: u64,
983 expected: &str,
984) -> Result<(), WireMismatch> {
985 match value.as_u64() {
986 Some(parsed) if parsed <= max => Ok(()),
987 _ => Err(WireMismatch::new(path, expected, describe(value))),
988 }
989}
990
991fn check_signed(
992 value: &serde_json::Value,
993 path: &str,
994 min: i64,
995 max: i64,
996 expected: &str,
997) -> Result<(), WireMismatch> {
998 match value.as_i64() {
999 Some(parsed) if (min..=max).contains(&parsed) => Ok(()),
1000 _ => Err(WireMismatch::new(path, expected, describe(value))),
1001 }
1002}
1003
1004fn describe(value: &serde_json::Value) -> String {
1005 match value {
1006 serde_json::Value::Null => String::from("null"),
1007 serde_json::Value::Bool(_) => String::from("a bool"),
1008 serde_json::Value::Number(number) => format!("the number {number}"),
1009 serde_json::Value::String(_) => String::from("a string"),
1010 serde_json::Value::Array(items) => format!("a sequence of {}", items.len()),
1011 serde_json::Value::Object(_) => String::from("a map"),
1012 }
1013}
1014
1015#[cfg(test)]
1016mod tests {
1017 use super::*;
1018
1019 fn field(name: &str, schema: WireSchema) -> WireField {
1020 WireField::required(name, schema)
1021 }
1022
1023 #[test]
1028 fn authoring_order_does_not_change_the_canonical_bytes() {
1029 let one = WireSchema::structure([
1030 field("alpha", WireSchema::U32),
1031 field("beta", WireSchema::String),
1032 field("gamma", WireSchema::Bool),
1033 ]);
1034 let other = WireSchema::structure([
1035 field("gamma", WireSchema::Bool),
1036 field("alpha", WireSchema::U32),
1037 field("beta", WireSchema::String),
1038 ]);
1039 assert_eq!(one.canonical_json(), other.canonical_json());
1040 assert_eq!(one, other);
1041
1042 let first = WireSchema::enumeration(
1043 EnumRepresentation::ExternallyTagged,
1044 [
1045 WireVariant::new("a", VariantBody::Unit),
1046 WireVariant::new("b", VariantBody::newtype(WireSchema::U8)),
1047 ],
1048 );
1049 let second = WireSchema::enumeration(
1050 EnumRepresentation::ExternallyTagged,
1051 [
1052 WireVariant::new("b", VariantBody::newtype(WireSchema::U8)),
1053 WireVariant::new("a", VariantBody::Unit),
1054 ],
1055 );
1056 assert_eq!(first.canonical_json(), second.canonical_json());
1057 }
1058
1059 #[test]
1062 fn positional_element_order_is_preserved() {
1063 let one = WireSchema::Tuple(vec![WireSchema::U8, WireSchema::String]);
1064 let other = WireSchema::Tuple(vec![WireSchema::String, WireSchema::U8]);
1065 assert_ne!(one.canonical_json(), other.canonical_json());
1066 }
1067
1068 #[test]
1073 fn untagged_variant_order_is_the_one_variant_order_that_survives() {
1074 let declared = [
1075 WireVariant::new("Zulu", VariantBody::newtype(WireSchema::I64)),
1076 WireVariant::new("Alpha", VariantBody::newtype(WireSchema::F64)),
1077 ];
1078 let untagged = WireSchema::enumeration(EnumRepresentation::Untagged, declared.clone());
1079 let WireSchema::Enum { variants, .. } = &untagged else {
1080 panic!("an enum");
1081 };
1082 assert_eq!(variants[0].name, "Zulu");
1083
1084 let tagged = WireSchema::enumeration(EnumRepresentation::ExternallyTagged, declared);
1085 let WireSchema::Enum { variants, .. } = &tagged else {
1086 panic!("an enum");
1087 };
1088 assert_eq!(variants[0].name, "Alpha");
1089 }
1090
1091 #[test]
1095 fn every_variant_renders_as_canonical_json() {
1096 let every = [
1097 WireSchema::Bool,
1098 WireSchema::U8,
1099 WireSchema::U16,
1100 WireSchema::U32,
1101 WireSchema::U64,
1102 WireSchema::I8,
1103 WireSchema::I16,
1104 WireSchema::I32,
1105 WireSchema::I64,
1106 WireSchema::F32,
1107 WireSchema::F64,
1108 WireSchema::String,
1109 WireSchema::Bytes,
1110 WireSchema::Unit,
1111 WireSchema::Freeform,
1112 WireSchema::option(WireSchema::U8),
1113 WireSchema::seq(WireSchema::U8),
1114 WireSchema::map(WireSchema::String, WireSchema::U8),
1115 WireSchema::Tuple(vec![WireSchema::U8, WireSchema::Bool]),
1116 WireSchema::array(WireSchema::U8, 4),
1117 WireSchema::structure([field("only", WireSchema::U8)]),
1118 WireSchema::newtype(WireSchema::String),
1119 WireSchema::enumeration(
1120 EnumRepresentation::InternallyTagged {
1121 tag: String::from("schema"),
1122 },
1123 [WireVariant::new(
1124 "v0",
1125 VariantBody::structure([field("value", WireSchema::U8)]),
1126 )],
1127 ),
1128 WireSchema::enumeration(
1129 EnumRepresentation::Untagged,
1130 [
1131 WireVariant::new("Text", VariantBody::newtype(WireSchema::String)),
1132 WireVariant::new("Count", VariantBody::Other),
1133 WireVariant::new("Pair", VariantBody::Tuple(vec![WireSchema::U8])),
1134 ],
1135 ),
1136 WireSchema::opaque("Digest", WireSchema::String),
1137 ];
1138 for schema in every {
1139 let rendered = schema.canonical_json();
1140 assert!(
1141 !rendered.contains(' ') && rendered.starts_with('{') && rendered.ends_with('}'),
1142 "{rendered}"
1143 );
1144 let parsed = serde_json::from_str::<serde_json::Value>(&rendered)
1145 .expect("the canonical rendering is JSON");
1146 assert!(parsed.is_object(), "{rendered}");
1147 assert_eq!(schema.canonical_json(), rendered);
1149 }
1150 }
1151
1152 #[test]
1153 fn nested_composition_renders_deterministically() {
1154 let schema = WireSchema::structure([
1155 field(
1156 "rows",
1157 WireSchema::seq(WireSchema::structure([
1158 field(
1159 "id",
1160 WireSchema::opaque("ParticipantId", WireSchema::String),
1161 ),
1162 WireField::new(
1163 "detail",
1164 WireSchema::option(WireSchema::String),
1165 FieldPresence::Optional,
1166 ),
1167 ])),
1168 ),
1169 field(
1170 "index",
1171 WireSchema::map(WireSchema::String, WireSchema::seq(WireSchema::U32)),
1172 ),
1173 ]);
1174 assert_eq!(
1175 schema.canonical_json(),
1176 concat!(
1177 r#"{"fields":["#,
1178 r#"{"name":"index","presence":"required","schema":"#,
1179 r#"{"key":{"kind":"string"},"kind":"map","value":{"item":{"kind":"u32"},"kind":"seq"}}},"#,
1180 r#"{"name":"rows","presence":"required","schema":{"item":{"fields":["#,
1181 r#"{"name":"detail","presence":"optional","schema":{"kind":"option","value":{"kind":"string"}}},"#,
1182 r#"{"name":"id","presence":"required","schema":{"kind":"opaque","name":"ParticipantId","wire":{"kind":"string"}}}"#,
1183 r#"],"kind":"struct"},"kind":"seq"}}"#,
1184 r#"],"kind":"struct"}"#,
1185 )
1186 );
1187 }
1188
1189 #[test]
1190 fn a_name_with_json_metacharacters_is_escaped() {
1191 let schema = WireSchema::opaque("a\"b\\c\nd", WireSchema::Unit);
1192 let rendered = schema.canonical_json();
1193 assert!(rendered.contains(r#""a\"b\\c\nd""#), "{rendered}");
1194 serde_json::from_str::<serde_json::Value>(&rendered).expect("still valid JSON");
1195 }
1196
1197 #[test]
1198 fn the_standard_impls_describe_the_shapes_serde_writes() {
1199 assert_eq!(
1200 <Option<u8>>::wire_schema(),
1201 WireSchema::option(WireSchema::U8)
1202 );
1203 assert_eq!(
1204 <Vec<String>>::wire_schema(),
1205 WireSchema::seq(WireSchema::String)
1206 );
1207 assert_eq!(
1208 <BTreeMap<String, u64>>::wire_schema(),
1209 WireSchema::map(WireSchema::String, WireSchema::U64)
1210 );
1211 assert_eq!(
1212 <[u8; 3]>::wire_schema(),
1213 WireSchema::array(WireSchema::U8, 3)
1214 );
1215 assert_eq!(
1216 <(u8, bool)>::wire_schema(),
1217 WireSchema::Tuple(vec![WireSchema::U8, WireSchema::Bool])
1218 );
1219 assert_eq!(<&str>::wire_schema(), WireSchema::String);
1220 assert_eq!(<()>::wire_schema(), WireSchema::Unit);
1221 assert_eq!(<NonZeroU64>::wire_schema(), WireSchema::U64);
1222 assert_eq!(
1223 <serde_json::Value>::wire_schema(),
1224 WireSchema::opaque("serde_json::Value", WireSchema::Freeform)
1225 );
1226 assert_eq!(<usize>::wire_schema(), WireSchema::U64);
1229 }
1230
1231 #[test]
1232 fn conformance_accepts_the_shape_and_names_the_first_disagreement() {
1233 let schema = WireSchema::structure([
1234 field("count", WireSchema::U8),
1235 WireField::new(
1236 "label",
1237 WireSchema::option(WireSchema::String),
1238 FieldPresence::Optional,
1239 ),
1240 ]);
1241 assert_eq!(
1242 schema.conforms(&serde_json::json!({"count": 7, "label": "ok"})),
1243 Ok(())
1244 );
1245 assert_eq!(schema.conforms(&serde_json::json!({"count": 7})), Ok(()));
1247
1248 let missing = schema
1249 .conforms(&serde_json::json!({"label": null}))
1250 .expect_err("a required field cannot be absent");
1251 assert_eq!(missing.path, ".count");
1252
1253 let extra = schema
1254 .conforms(&serde_json::json!({"count": 1, "label": null, "surprise": 2}))
1255 .expect_err("an undeclared field is a disagreement");
1256 assert_eq!(extra.path, ".surprise");
1257
1258 let too_wide = schema
1259 .conforms(&serde_json::json!({"count": 300}))
1260 .expect_err("an out-of-range integer is not a u8");
1261 assert_eq!(too_wide.expected, "u8");
1262 }
1263
1264 #[test]
1265 fn conformance_reads_each_enum_representation_the_way_serde_writes_it() {
1266 let external = WireSchema::enumeration(
1267 EnumRepresentation::ExternallyTagged,
1268 [
1269 WireVariant::new("stop", VariantBody::Unit),
1270 WireVariant::new(
1271 "go",
1272 VariantBody::structure([field("speed", WireSchema::F32)]),
1273 ),
1274 ],
1275 );
1276 assert_eq!(external.conforms(&serde_json::json!("stop")), Ok(()));
1277 assert_eq!(
1278 external.conforms(&serde_json::json!({"go": {"speed": 1.5}})),
1279 Ok(())
1280 );
1281 assert!(external.conforms(&serde_json::json!("fly")).is_err());
1282
1283 let internal = WireSchema::enumeration(
1284 EnumRepresentation::InternallyTagged {
1285 tag: String::from("schema"),
1286 },
1287 [WireVariant::new(
1288 "phoxal/example/v0",
1289 VariantBody::structure([field("value", WireSchema::U8)]),
1290 )],
1291 );
1292 assert_eq!(
1293 internal.conforms(&serde_json::json!({"schema": "phoxal/example/v0", "value": 3})),
1294 Ok(())
1295 );
1296 assert!(
1297 internal
1298 .conforms(&serde_json::json!({"schema": "phoxal/example/v1", "value": 3}))
1299 .is_err()
1300 );
1301
1302 let merged = WireSchema::enumeration(
1305 EnumRepresentation::InternallyTagged {
1306 tag: String::from("schema"),
1307 },
1308 [WireVariant::new(
1309 "phoxal/example/v0",
1310 VariantBody::newtype(WireSchema::opaque(
1311 "Inner",
1312 WireSchema::structure([field("value", WireSchema::U8)]),
1313 )),
1314 )],
1315 );
1316 assert_eq!(
1317 merged.conforms(&serde_json::json!({"schema": "phoxal/example/v0", "value": 3})),
1318 Ok(())
1319 );
1320
1321 let untagged = WireSchema::enumeration(
1322 EnumRepresentation::Untagged,
1323 [
1324 WireVariant::new("Bool", VariantBody::newtype(WireSchema::Bool)),
1325 WireVariant::new("Text", VariantBody::newtype(WireSchema::String)),
1326 ],
1327 );
1328 assert_eq!(untagged.conforms(&serde_json::json!(true)), Ok(()));
1329 assert_eq!(untagged.conforms(&serde_json::json!("hello")), Ok(()));
1330 assert!(untagged.conforms(&serde_json::json!(1)).is_err());
1331 }
1332
1333 #[test]
1334 fn conformance_reads_a_byte_string_in_both_renderings() {
1335 assert_eq!(
1336 WireSchema::Bytes.conforms(&serde_json::json!([1, 2, 255])),
1337 Ok(())
1338 );
1339 assert_eq!(WireSchema::Bytes.conforms(&serde_json::json!("ab")), Ok(()));
1340 assert!(
1341 WireSchema::Bytes
1342 .conforms(&serde_json::json!([256]))
1343 .is_err()
1344 );
1345 }
1346
1347 #[test]
1348 fn conformance_checks_a_maps_keys_as_well_as_its_values() {
1349 let text_keys = WireSchema::map(
1350 WireSchema::opaque("CapabilityId", WireSchema::String),
1351 WireSchema::I8,
1352 );
1353 assert_eq!(
1354 text_keys.conforms(&serde_json::json!({"wheel": -1})),
1355 Ok(())
1356 );
1357 assert!(
1358 text_keys
1359 .conforms(&serde_json::json!({"wheel": 900}))
1360 .is_err()
1361 );
1362
1363 let numeric_keys = WireSchema::map(WireSchema::U32, WireSchema::Bool);
1366 assert_eq!(
1367 numeric_keys.conforms(&serde_json::json!({"7": true})),
1368 Ok(())
1369 );
1370 assert!(
1371 numeric_keys
1372 .conforms(&serde_json::json!({"seven": true}))
1373 .is_err()
1374 );
1375 }
1376
1377 #[test]
1380 fn transparent_wrappers_resolve_to_the_shape_underneath() {
1381 let schema = WireSchema::opaque(
1382 "Wrapper",
1383 WireSchema::newtype(WireSchema::structure([field("value", WireSchema::U8)])),
1384 );
1385 assert!(matches!(schema.resolved(), WireSchema::Struct { .. }));
1386 assert_eq!(schema.conforms(&serde_json::json!({"value": 1})), Ok(()));
1387 }
1388}