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]
29pub 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 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 #[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 if !s.starts_with(#prefix) {
203 return Err(::puid::Error::InvalidPrefix);
204 }
205 if s.as_bytes()[#prefix_len] != b'_' {
207 return Err(::puid::Error::InvalidFormat);
208 }
209 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}