Skip to main content

stefans_utils/
literals.rs

1#[macro_export]
2macro_rules! literal {
3    ($(#[$attr:meta])* $name:ident($type:ty) = $value:expr) => {
4        #[derive(::core::fmt::Debug, ::core::clone::Clone, ::serde::Serialize)]
5        $(#[$attr])*
6        pub struct $name($type);
7
8        impl<'de> ::serde::Deserialize<'de> for $name {
9            fn deserialize<D>(deserializer: D) -> ::core::result::Result<Self, D::Error>
10            where
11                D: ::serde::Deserializer<'de>,
12            {
13                let value = <$type as ::serde::Deserialize<'de>>::deserialize(deserializer)?;
14
15                Self::try_from(value).map_err(::serde::de::Error::custom)
16            }
17        }
18
19        impl<T: PartialEq<$type>> PartialEq<T> for $name {
20            fn eq(&self, other: &T) -> bool {
21                other.eq(&self.0)
22            }
23        }
24
25        impl ::schemars::JsonSchema for $name {
26            fn schema_name() -> ::std::borrow::Cow<'static, str> {
27                stringify!($name).into()
28            }
29
30            fn json_schema(_: &mut ::schemars::SchemaGenerator) -> ::schemars::Schema {
31                ::schemars::json_schema!({ "const": $value })
32            }
33        }
34
35        impl ::std::fmt::Display for $name {
36            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
37                f.write_fmt(format_args!("{}", &self.0))
38            }
39        }
40
41        impl AsRef<$type> for $name {
42            fn as_ref(&self) -> &$type {
43                &self.0
44            }
45        }
46
47        impl TryFrom<$type> for $name {
48            type Error = String;
49
50            fn try_from(value: $type) -> ::std::result::Result<Self, Self::Error> {
51                if value == $value {
52                    Ok(Self(value))
53                } else {
54                    Err(format!("Value must be exactly {:?}", $value))
55                }
56            }
57        }
58
59        impl From<$name> for $type {
60            fn from(value: $name) -> Self {
61                value.0
62            }
63        }
64    };
65}
66
67#[macro_export]
68macro_rules! literal_string {
69    ($name:ident = $value:expr) => {
70        $crate::literal!($name(String) = $value);
71
72        impl Default for $name {
73            fn default() -> Self {
74                Self($value.to_string())
75            }
76        }
77    };
78}
79
80macro_rules! literal_scalars {
81    ($d:tt $($type:ident)*) => {
82        $(
83            ::paste::paste! {
84                #[macro_export]
85                macro_rules! [<literal_ $type>] {
86                    ($d name:ident = $d value:expr) => {
87                        $crate::literal!($d name($type) = $d value);
88
89                        impl Default for $d name {
90                            fn default() -> Self {
91                                Self($d value)
92                            }
93                        }
94                    };
95                }
96            }
97        )*
98    };
99}
100
101literal_scalars!($ u8 u16 u32 u64 u128 usize i8 i16 i32 i64 i128 isize f32 f64 bool);
102
103#[cfg(test)]
104mod tests {
105    use schemars::{json_schema, schema_for};
106    use serde_json::json;
107
108    crate::literal_string!(Xbox = "xbox");
109
110    #[test]
111    fn literal_string_should_deserialize_ok_from_correct_value() {
112        let ok = serde_json::from_value::<Xbox>(json!("xbox")).unwrap();
113
114        assert_eq!(ok, "xbox");
115    }
116
117    #[test]
118    fn literal_string_should_deserialize_err_from_correct_type_but_wrong_value() {
119        let err = serde_json::from_value::<Xbox>(json!("ps4")).unwrap_err();
120
121        assert_eq!(err.to_string(), "Value must be exactly \"xbox\"");
122    }
123
124    #[test]
125    fn literal_string_should_produce_correct_json_schema() {
126        let schema = schema_for!(Xbox);
127
128        assert_eq!(
129            schema,
130            json_schema!({
131                "$schema": "https://json-schema.org/draft/2020-12/schema",
132                "title": "Xbox",
133                "const": "xbox"
134            })
135        );
136    }
137
138    literal_u32!(OneTwoThree = 123);
139
140    #[test]
141    fn literal_u32_should_deserialize_ok_from_correct_value() {
142        let ok = serde_json::from_value::<OneTwoThree>(json!(123)).unwrap();
143
144        assert_eq!(ok, 123);
145    }
146
147    #[test]
148    fn literal_u32_should_deserialize_err_from_correct_type_but_wrong_value() {
149        let err = serde_json::from_value::<OneTwoThree>(json!(321)).unwrap_err();
150
151        assert_eq!(err.to_string(), "Value must be exactly 123");
152    }
153
154    #[test]
155    fn literal_u32_should_produce_correct_json_schema() {
156        let schema = schema_for!(OneTwoThree);
157
158        assert_eq!(
159            schema,
160            json_schema!({
161                "$schema": "https://json-schema.org/draft/2020-12/schema",
162                "title": "OneTwoThree",
163                "const": 123
164            })
165        );
166    }
167}