Skip to main content

puid_macros/
lib.rs

1use proc_macro2::TokenStream;
2use quote::quote;
3use syn::{
4    parse::{Parse, ParseStream},
5    parse_macro_input,
6    Ident,
7    LitStr,
8    Result,
9};
10
11struct Input {
12    name:   syn::Ident,
13    prefix: String,
14}
15
16impl Parse for Input {
17    fn parse(input: ParseStream) -> Result<Self> {
18        let name: Ident = input.parse()?;
19        input.parse::<syn::Token![=]>()?;
20        let prefix: LitStr = input.parse()?;
21        Ok(Input {
22            name,
23            prefix: prefix.value(),
24        })
25    }
26}
27
28#[proc_macro]
29/// Generates a type for a Prefixed Unique Identifier (PUID).
30///
31/// # Example:
32/// ```
33/// puid!(UserId = "usr");
34/// let user_id = UserId::new();
35/// assert_eq!(user_id.as_str().len(), 26); // "usr_" + _ + 22 base62 characters
36/// assert!(user_id.as_str().starts_with("usr_"));
37/// ```
38pub fn puid(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
39    impl_puid(parse_macro_input!(input as Input))
40        .unwrap()
41        .into()
42}
43
44fn impl_puid(Input { name, prefix }: Input) -> Result<TokenStream> {
45    // 22 bytes for the suffix and 1 byte for the underscore
46    let prefix_len = prefix.len();
47    let len = prefix_len + 22 + 1;
48    let mut buf = Vec::with_capacity(len);
49    for i in prefix.bytes() {
50        buf.push(i);
51    }
52    buf.push(b'_');
53    buf.resize(len, b'0');
54
55    let serde = if cfg!(feature = "serde") {
56        let visitor_ident = syn::Ident::new(&format!("{name}SerdeVisitor"), name.span());
57        quote! {
58            impl ::serde::Serialize for #name {
59                fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
60                where
61                    S: ::serde::Serializer,
62                {
63                    serializer.serialize_str(self.as_str())
64                }
65            }
66
67            struct #visitor_ident;
68
69            impl ::serde::de::Visitor<'_> for #visitor_ident {
70                type Value = #name;
71
72                fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
73                    formatter.write_str("a string with the format '#prefix_<suffix>'")
74                }
75
76                fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
77                where
78                    E: ::serde::de::Error,
79                {
80                    v.parse().map_err(E::custom)
81                }
82            }
83
84            impl<'de> ::serde::Deserialize<'de> for #name {
85                fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
86                where
87                    D: ::serde::Deserializer<'de>,
88                {
89                    deserializer.deserialize_str(#visitor_ident)
90                }
91            }
92        }
93    } else {
94        quote! {}
95    };
96
97    let snake_case_name = {
98        let mut name_str = name.to_string();
99        for c in 'A'..='Z' {
100            name_str = name_str.replace(c, &format!("_{}", c.to_ascii_lowercase()));
101        }
102        name_str.trim_start_matches('_').to_string()
103    };
104    let postgres = if cfg!(feature = "postgres") {
105        quote! {
106            impl ::sqlx::Type<::sqlx::Postgres> for #name {
107                fn type_info() -> ::sqlx::postgres::PgTypeInfo {
108                    ::sqlx::postgres::PgTypeInfo::with_name(#snake_case_name)
109                }
110
111                fn compatible(ty: &::sqlx::postgres::PgTypeInfo) -> bool {
112                    ty == &::sqlx::postgres::PgTypeInfo::with_name("user_id") || <&str as ::sqlx::Type<::sqlx::Postgres>>::compatible(ty)
113                }
114            }
115
116            impl ::sqlx::Encode<'_, ::sqlx::Postgres> for #name {
117                fn encode_by_ref(&self, buf: &mut ::sqlx::postgres::PgArgumentBuffer) -> ::std::result::Result<::sqlx::encode::IsNull, ::sqlx::error::BoxDynError> {
118                    buf.extend(self.as_str().as_bytes());
119                    Ok(::sqlx::encode::IsNull::No)
120                }
121            }
122
123            impl<'r> ::sqlx::Decode<'r, ::sqlx::Postgres> for #name {
124                fn decode(value: ::sqlx::postgres::PgValueRef<'r>) -> ::std::result::Result<Self, ::sqlx::error::BoxDynError> {
125                    let s: &str = value.as_str()?;
126                    s.parse().map_err(::std::convert::Into::into)
127                }
128            }
129        }
130    } else {
131        quote! {}
132    };
133
134    let sea_query = if cfg!(feature = "sea-query") {
135        quote! {
136            impl From<#name> for ::sea_query::Value {
137                fn from(value: #name) -> Self {
138                    ::sea_query::Value::String(Some(value.to_string().into()))
139                }
140            }
141
142            impl ::sea_query::value::Nullable for #name {
143                fn null() -> ::sea_query::Value {
144                    ::sea_query::Value::String(None)
145                }
146            }
147        }
148    } else {
149        quote! {}
150    };
151
152    let create_domain = LitStr::new(
153        &format!(
154            "CREATE DOMAIN {snake_case_name} AS CHAR({len}) CHECK (VALUE ~ \
155             '^{prefix}_[0-9A-Za-z]{{22}}$');",
156        ),
157        proc_macro2::Span::call_site(),
158    );
159
160    Ok(quote! {
161        #[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
162        pub struct #name([u8; #len]);
163
164        impl #name {
165            /// Creates a new `#name` with the given suffix.
166            #[allow(clippy::new_without_default)]
167            pub fn new() -> Self {
168                let mut buf = [#(#buf),*];
169
170                ::puid::encode_suffix(&mut buf[#prefix_len + 1..]);
171                #name(buf)
172            }
173
174            pub fn as_str(&self) -> &str {
175                unsafe {
176                    ::std::str::from_utf8_unchecked(&self.0)
177                }
178            }
179
180            pub fn create_domain() -> &'static str {
181                #create_domain
182            }
183
184            pub fn nil() -> Self {
185                #name([#(#buf),*])
186            }
187
188            pub fn is_nil(&self) -> bool {
189                self.0 == [#(#buf),*]
190            }
191        }
192
193        impl ::std::str::FromStr for #name {
194            type Err = ::puid::Error;
195
196            fn from_str(s: &str) -> Result<Self, Self::Err> {
197                if s.len() != #len {
198                    return Err(::puid::Error::InvalidLength);
199                }
200                let mut buf = [#(#buf),*];
201                // ensure the prefix matches
202                if !s.starts_with(#prefix) {
203                    return Err(::puid::Error::InvalidPrefix);
204                }
205                // ensure the next byte is an underscore
206                if s.as_bytes()[#prefix_len] != b'_' {
207                    return Err(::puid::Error::InvalidFormat);
208                }
209                // ensure the suffix is valid then copy
210                for c in &s.as_bytes()[#prefix_len + 1..] {
211                    if !::puid::is_valid_suffix_byte(*c) {
212                        return Err(::puid::Error::InvalidSuffixChar(*c));
213                    }
214                }
215                buf[#prefix_len + 1..].copy_from_slice(&s.as_bytes()[#prefix_len + 1..]);
216                Ok(#name(buf))
217            }
218        }
219
220        impl ::std::fmt::Display for #name {
221            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
222                f.write_str(self.as_str())
223            }
224        }
225
226        impl ::std::fmt::Debug for #name {
227            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
228                f.debug_struct(stringify!(#name))
229                    .field("value", &self.as_str())
230                    .finish()
231            }
232        }
233
234        impl From<&str> for #name {
235            fn from(s: &str) -> Self {
236                s.parse().expect("Invalid PUID string")
237            }
238        }
239
240        impl From<String> for #name {
241            fn from(s: String) -> Self {
242                s.parse().expect("Invalid PUID string")
243            }
244        }
245
246        #serde
247
248        #postgres
249
250        #sea_query
251    })
252}