Skip to main content

citum_schema_data/
macros.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6/// Generates a string-backed enum that gracefully captures unknown variants.
7///
8/// The macro handles custom `serde::Deserialize` and `serde::Serialize` to ensure
9/// unknown string values are captured into an `Unknown(String)` variant instead
10/// of failing the parse. It also configures `schemars` and `specta` to skip the
11/// `Unknown` variant, maintaining a strictly closed public schema for producers.
12#[macro_export]
13macro_rules! tolerant_enum {
14    (
15        $(#[$meta:meta])*
16        $vis:vis enum $name:ident {
17            $(
18                $(#[$vmeta:meta])*
19                $variant:ident = $val:expr
20            ),+ $(,)?
21        }
22    ) => {
23        $(#[$meta])*
24        #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
25        #[cfg_attr(feature = "bindings", derive(specta::Type))]
26        #[non_exhaustive]
27        $vis enum $name {
28            $(
29                $(#[$vmeta])*
30                #[cfg_attr(any(feature = "schema", feature = "bindings"), serde(rename = $val))]
31                $variant,
32            )+
33            #[doc = "Fallback for forward-compatibility."]
34            #[cfg_attr(feature = "schema", schemars(skip))]
35            #[cfg_attr(feature = "bindings", specta(skip))]
36            Unknown(String),
37        }
38
39        impl $name {
40            #[doc = "Returns the string value associated with this variant."]
41            #[must_use]
42            pub fn as_str(&self) -> &str {
43                match self {
44                    $( Self::$variant => $val, )+
45                    Self::Unknown(s) => s.as_str(),
46                }
47            }
48        }
49
50        impl serde::Serialize for $name {
51            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
52            where
53                S: serde::Serializer,
54            {
55                serializer.serialize_str(self.as_str())
56            }
57        }
58
59        impl<'de> serde::Deserialize<'de> for $name {
60            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
61            where
62                D: serde::Deserializer<'de>,
63            {
64                struct Visitor;
65                impl<'de> serde::de::Visitor<'de> for Visitor {
66                    type Value = $name;
67
68                    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
69                        formatter.write_str("a string")
70                    }
71
72                    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
73                    where
74                        E: serde::de::Error,
75                    {
76                        Ok(match value {
77                            $( $val => $name::$variant, )+
78                            _ => $name::Unknown(value.to_owned()),
79                        })
80                    }
81                }
82                deserializer.deserialize_str(Visitor)
83            }
84        }
85    }
86}