Skip to main content

stefans_utils/
literals.rs

1#[macro_export]
2macro_rules! literal {
3    ($(#[$attr:meta])* $name:ident($type:tt) = $value:literal) => {
4        $crate::literal!($(#[$attr])* $name($type => $type) = $value);
5    };
6
7    ($(#[$attr:meta])* $name:ident($serde_from:tt => $type:tt) = $value:literal) => {
8        $crate::literal!(@ $(#[$attr])* $name($serde_from => $type) = $value);
9
10        impl $name {
11            pub const VALUE: $type = $value;
12        }
13
14        impl<T: PartialEq<$type>> PartialEq<T> for $name {
15            fn eq(&self, other: &T) -> bool {
16                other.eq(&Self::VALUE)
17            }
18        }
19    };
20
21    ($(#[$attr:meta])* $name:ident(&$type:tt) = $value:literal) => {
22        $crate::literal!($(#[$attr])* $name($type => $type) = $value);
23    };
24
25    ($(#[$attr:meta])* $name:ident($serde_from:tt => &$type:tt) = $value:literal) => {
26        $crate::literal!(@ $(#[$attr])* $name($serde_from => $type) = $value);
27
28        impl $name {
29            pub const VALUE: &$type = $value;
30        }
31
32        impl<T: for<'a> PartialEq<&'a $type>> PartialEq<T> for $name {
33            fn eq(&self, other: &T) -> bool {
34                other.eq(&Self::VALUE)
35            }
36        }
37    };
38
39    (@ $(#[$attr:meta])*  $name:ident($serde_from:tt => $type:tt) = $value:literal) => {
40        #[derive(Clone, Copy)]
41        $(#[$attr])*
42        pub struct $name;
43
44        impl AsRef<$type> for $name {
45            fn as_ref(&self) -> &$type {
46                &Self::VALUE
47            }
48        }
49
50        impl ::std::fmt::Debug for $name {
51            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52                f.debug_tuple($name::NAME).field(&$name::VALUE).finish()
53            }
54        }
55
56        impl $name {
57            const NAME: &'static str = stringify!($name);
58        }
59
60        impl Default for $name {
61            fn default() -> Self {
62                Self
63            }
64        }
65
66        impl ::serde::Serialize for $name {
67            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
68            where
69                S: ::serde::Serializer,
70            {
71                serializer.serialize_newtype_struct($name::NAME, &$name::VALUE)
72            }
73        }
74
75        impl<'de> ::serde::Deserialize<'de> for $name {
76            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
77            where
78                D: ::serde::Deserializer<'de>,
79            {
80                <$serde_from as ::serde::Deserialize<'de>>::deserialize(deserializer).and_then(
81                    |value| {
82                        if value == $name::VALUE {
83                            Ok($name)
84                        } else {
85                            Err(::serde::de::Error::custom(concat!(
86                                "Value must be exactly ",
87                                stringify!($value)
88                            )))
89                        }
90                    },
91                )
92            }
93        }
94
95        impl ::schemars::JsonSchema for $name {
96            fn schema_name() -> ::std::borrow::Cow<'static, str> {
97                $name::NAME.into()
98            }
99
100            fn json_schema(_: &mut ::schemars::SchemaGenerator) -> ::schemars::Schema {
101                ::schemars::json_schema!({
102                    "const": $name::VALUE
103                })
104            }
105        }
106
107        impl Eq for $name {}
108    };
109}
110
111#[macro_export]
112macro_rules! literal_str {
113    ($(#[$attr:meta])* $name:ident = $value:literal) => {
114        $crate::literal!($(#[$attr])* $name(String => &str) = $value);
115    };
116}
117
118#[macro_export]
119macro_rules! literal_bool {
120    ($(#[$attr:meta])* $name:ident = true) => {
121        $crate::literal!($(#[$attr])* $name(bool) = true);
122        impl ::std::ops::Not for $name {
123            type Output = bool;
124            fn not(self) -> bool {
125                false
126            }
127        }
128    };
129
130    ($(#[$attr:meta])* $name:ident = false) => {
131        $crate::literal!($(#[$attr])* $name(bool) = false);
132        impl ::std::ops::Not for $name {
133            type Output = bool;
134            fn not(self) -> bool {
135                true
136            }
137        }
138    };
139}
140
141macro_rules! literal_scalars {
142    ($d:tt $($type:ident)*) => {
143        $(
144            ::paste::paste! {
145                #[macro_export]
146                macro_rules! [<literal_ $type>] {
147                    ($d (#[$d attr:meta])* $d name:ident = $d value:literal) => {
148                        $crate::literal!($d (#[$d attr])* $d name ($type) = $d value);
149                    }
150                }
151            }
152        )*
153    }
154}
155
156literal_scalars!($ u8 u16 u32 u64 u128 usize i8 i16 i32 i64 i128 isize f32 f64);
157
158// export actual types because what the hell
159literal_bool!(True = true);
160literal_bool!(False = false);
161
162#[cfg(test)]
163mod tests {
164
165    #[test]
166    #[allow(clippy::bool_assert_comparison)]
167    fn literal_bool_should_work() {
168        use super::{False, True};
169
170        assert_eq!(True, True);
171        assert_ne!(True, False);
172        assert_ne!(False, True);
173        assert_eq!(False, False);
174
175        assert_eq!(True, true);
176        assert_ne!(True, false);
177        assert_ne!(False, true);
178        assert_eq!(False, false);
179
180        assert!(True);
181        assert!(!False);
182    }
183
184    #[test]
185    fn literal_str_should_work() {
186        literal_str!(
187            #[doc = "metametameta"]
188            MyStr = "my-value"
189        );
190
191        assert_eq!(
192            serde_json::from_str::<MyStr>("\"my-value\"").unwrap(),
193            "my-value"
194        );
195
196        assert_eq!(MyStr, MyStr);
197    }
198
199    #[test]
200    fn literal_u8_should_work() {
201        literal_u8!(Two = 2);
202        literal_u8!(
203            #[doc = "lololol"]
204            AlsoTwo = 2
205        );
206
207        assert_eq!(Two, 2);
208        assert_eq!(Two, Two);
209        assert_eq!(Two, AlsoTwo);
210        assert_eq!(AlsoTwo, Two);
211        assert_ne!(Two, 3);
212    }
213    use schemars::{json_schema, schema_for};
214    use serde_json::json;
215
216    crate::literal_str!(Xbox = "xbox");
217
218    #[test]
219    fn literal_string_should_deserialize_ok_from_correct_value() {
220        let ok = serde_json::from_value::<Xbox>(json!("xbox")).unwrap();
221
222        assert_eq!(ok, "xbox");
223    }
224
225    #[test]
226    fn literal_string_should_deserialize_err_from_correct_type_but_wrong_value() {
227        let err = serde_json::from_value::<Xbox>(json!("ps4")).unwrap_err();
228
229        assert_eq!(err.to_string(), "Value must be exactly \"xbox\"");
230    }
231
232    #[test]
233    fn literal_string_should_produce_correct_json_schema() {
234        let schema = schema_for!(Xbox);
235
236        assert_eq!(
237            schema,
238            json_schema!({
239                "$schema": "https://json-schema.org/draft/2020-12/schema",
240                "title": "Xbox",
241                "const": "xbox"
242            })
243        );
244    }
245
246    literal_u32!(OneTwoThree = 123);
247
248    #[test]
249    fn literal_u32_should_deserialize_ok_from_correct_value() {
250        let ok = serde_json::from_value::<OneTwoThree>(json!(123)).unwrap();
251
252        assert_eq!(ok, 123);
253    }
254
255    #[test]
256    fn literal_u32_should_deserialize_err_from_correct_type_but_wrong_value() {
257        let err = serde_json::from_value::<OneTwoThree>(json!(321)).unwrap_err();
258
259        assert_eq!(err.to_string(), "Value must be exactly 123");
260    }
261
262    #[test]
263    fn literal_u32_should_produce_correct_json_schema() {
264        let schema = schema_for!(OneTwoThree);
265
266        assert_eq!(
267            schema,
268            json_schema!({
269                "$schema": "https://json-schema.org/draft/2020-12/schema",
270                "title": "OneTwoThree",
271                "const": 123
272            })
273        );
274    }
275}