Skip to main content

newt_interaction/
tag.rs

1//! The versioned type tag, as a TYPE.
2//!
3//! A `schema: String` field lets a record claim any tag, and — more to the
4//! point for the slice that publishes a cross-language contract — it makes
5//! the generated JSON Schema say `{"type": "string"}`. A foreign
6//! implementor validating a definition record against `response.schema.json`
7//! would PASS, while our decoder classifies it unknown. That is not a
8//! description of "response v1"; it is a description of "any record with
9//! some string in its schema field".
10//!
11//! Each record therefore carries a tag type that serializes and
12//! deserializes exactly one value, and whose generated schema is a
13//! `const`. The outer PROBE stays permissive on purpose — it reads the tag
14//! as a plain string and dispatches to the strict deserializer only on an
15//! exact match, which is what lets an unknown version be preserved rather
16//! than rejected as malformed.
17
18use serde::de::{Error as _, Unexpected};
19use serde::{Deserialize, Deserializer, Serialize, Serializer};
20
21/// Define a tag type that is exactly one string on the wire.
22macro_rules! schema_tag {
23    ($(#[$meta:meta])* $name:ident, $value:path) => {
24        $(#[$meta])*
25        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
26        pub struct $name;
27
28        impl $name {
29            /// The one value this tag takes on the wire.
30            #[must_use]
31            pub fn as_str(&self) -> &'static str {
32                $value
33            }
34        }
35
36        impl Serialize for $name {
37            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
38                serializer.serialize_str($value)
39            }
40        }
41
42        impl<'de> Deserialize<'de> for $name {
43            fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
44                let text = String::deserialize(deserializer)?;
45                if text == $value {
46                    Ok(Self)
47                } else {
48                    Err(D::Error::invalid_value(
49                        Unexpected::Str(&text),
50                        &$value,
51                    ))
52                }
53            }
54        }
55
56        impl std::fmt::Display for $name {
57            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58                f.write_str($value)
59            }
60        }
61
62        #[cfg(feature = "schema")]
63        impl schemars::JsonSchema for $name {
64            fn schema_name() -> String {
65                stringify!($name).to_string()
66            }
67
68            fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
69                let mut schema = schemars::schema::SchemaObject {
70                    instance_type: Some(schemars::schema::InstanceType::String.into()),
71                    ..Default::default()
72                };
73                schema.const_value = Some(serde_json::Value::String($value.to_string()));
74                schema.into()
75            }
76        }
77    };
78}
79
80schema_tag!(
81    /// The tag every v1 definition carries, and the only value it accepts.
82    DefinitionTag,
83    crate::definition::DEFINITION_SCHEMA_V1
84);
85schema_tag!(
86    /// The tag every v1 instance carries.
87    InstanceTag,
88    crate::instance::INSTANCE_SCHEMA_V1
89);
90schema_tag!(
91    /// The tag every v1 response carries.
92    ResponseTag,
93    crate::response::RESPONSE_SCHEMA_V1
94);
95
96/// Publish a constrained string schema for a validated scalar newtype.
97///
98/// A derived `JsonSchema` on these would emit a bare `{"type": "string"}`,
99/// which permits values the decoder refuses — the same false-guarantee
100/// defect as a doc comment claiming a validation the wire does not
101/// perform. A schema is a published contract; it has to state the rule.
102#[cfg(feature = "schema")]
103#[macro_export]
104macro_rules! string_scalar_schema {
105    ($name:ident, $pattern:expr) => {
106        impl schemars::JsonSchema for $name {
107            fn schema_name() -> String {
108                stringify!($name).to_string()
109            }
110
111            fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
112                let mut schema = schemars::schema::SchemaObject {
113                    instance_type: Some(schemars::schema::InstanceType::String.into()),
114                    ..Default::default()
115                };
116                schema.string().min_length = Some(1);
117                if let Some(pattern) = $pattern {
118                    schema.string().pattern = Some(pattern.to_string());
119                }
120                schema.into()
121            }
122        }
123    };
124}