Skip to main content

uuid_enum_proc_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use quote::quote;
4use std::cell::RefCell;
5use std::collections::HashMap;
6use syn::{
7    parse_macro_input, spanned::Spanned, Attribute, Expr, ExprLit, ItemEnum, Lit, LitInt, LitStr,
8    Meta, MetaNameValue, Variant,
9};
10
11thread_local! {
12    /// Global registry of seen UUID values (as u128) within this compilation unit.
13    ///
14    /// This enforces "no UUID reuse" across all #[uuid_enum] enums in the same crate.
15    static SEEN_UUIDS: RefCell<HashMap<u128, Span>> = RefCell::new(HashMap::new());
16}
17
18/// Attribute macro for UUID-backed enums.
19///
20/// Usage:
21///
22/// ```ignore
23/// use uuid_enum::uuid_enum;
24///
25/// #[uuid_enum]
26/// pub enum AccountGrant {
27///     #[uuid("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")]
28///     Owner,
29///     #[uuid("cccccccc-cccc-cccc-cccc-cccccccccccc")]
30///     Manage,
31/// }
32/// ```
33///
34/// This expands to a `#[repr(u128)]` enum where each variant's discriminant
35/// is the numerical `u128` value of its UUID. It also generates:
36///
37/// - `const ALL: &'static [Self]` containing all variants
38/// - `pub const fn as_u128(self) -> u128`
39/// - `pub const fn from_u128(raw: u128) -> Option<Self>`
40/// - `pub const fn to_uuid(self) -> ::uuid_enum::uuid::Uuid`
41/// - `pub const fn from_uuid(id: ::uuid_enum::uuid::Uuid) -> Option<Self>`
42///
43/// The UUID values are checked for global uniqueness across the crate.
44/// Reuse of the same UUID in two variants (even in different enums using this
45/// macro) is a compile error.
46#[proc_macro_attribute]
47pub fn uuid_enum(attr: TokenStream, item: TokenStream) -> TokenStream {
48    // We don't currently support any arguments on #[uuid_enum].
49    if !attr.is_empty() {
50        let ts = proc_macro2::TokenStream::from(attr);
51        return syn::Error::new_spanned(ts, "#[uuid_enum] does not take any arguments")
52            .to_compile_error()
53            .into();
54    }
55
56    let input = parse_macro_input!(item as ItemEnum);
57
58    match expand_uuid_enum(input) {
59        Ok(ts) => ts,
60        Err(e) => e.to_compile_error().into(),
61    }
62}
63
64fn expand_uuid_enum(item: ItemEnum) -> syn::Result<TokenStream> {
65    // We keep the enum's visibility, name, and generics, but we currently only
66    // support non-generic enums.
67    if !item.generics.params.is_empty() {
68        return Err(syn::Error::new_spanned(
69            &item.generics,
70            "#[uuid_enum] currently only supports non-generic enums",
71        ));
72    }
73
74    // Reject enums with an existing #[repr(...)] attribute to avoid conflicts.
75    for attr in &item.attrs {
76        if attr.path().is_ident("repr") {
77            return Err(syn::Error::new_spanned(
78                attr,
79                "#[uuid_enum] must not be combined with an explicit #[repr(..)] on the enum",
80            ));
81        }
82    }
83
84    let ident = &item.ident;
85
86    // For each variant, we will compute a u128 discriminant from its #[uuid("…")] attribute.
87    let mut variant_idents = Vec::new();
88    let mut variant_values = Vec::new();
89
90    for variant in &item.variants {
91        ensure_unit_variant(variant)?;
92
93        if variant.discriminant.is_some() {
94            return Err(syn::Error::new_spanned(
95                variant,
96                "#[uuid_enum] does not allow explicit discriminants; they are derived from #[uuid(\"…\")]",
97            ));
98        }
99
100        let uuid_attr = find_uuid_attribute(&variant.attrs).ok_or_else(|| {
101            syn::Error::new(
102                variant.span(),
103                "each variant in a #[uuid_enum] must have a #[uuid(\"…\")] attribute",
104            )
105        })?;
106
107        let uuid_str = parse_uuid_attribute(uuid_attr)?;
108        let uuid = uuid::Uuid::parse_str(&uuid_str).map_err(|e| {
109            syn::Error::new(
110                uuid_attr.span(),
111                format!("invalid UUID string in #[uuid(..)]: {e}"),
112            )
113        })?;
114        let value = uuid.as_u128();
115
116        // Global uniqueness check across all enums using this macro in this crate.
117        let dup = SEEN_UUIDS.with(|cell| {
118            let mut map = cell.borrow_mut();
119            if let Some(prev_span) = map.get(&value) {
120                Some(*prev_span)
121            } else {
122                map.insert(value, uuid_attr.span());
123                None
124            }
125        });
126
127        if let Some(prev_span) = dup {
128            let mut err = syn::Error::new(
129                uuid_attr.span(),
130                "this UUID is already used elsewhere in this crate",
131            );
132            err.combine(syn::Error::new(
133                prev_span,
134                "previous use of this UUID is here",
135            ));
136            return Err(err);
137        }
138
139        variant_idents.push(&variant.ident);
140        variant_values.push(value);
141    }
142
143    // Reconstruct the enum with #[repr(u128)] and explicit numeric discriminants.
144    let mut new_enum = item.clone();
145    new_enum.attrs.push(syn::parse_quote!(#[repr(u128)]));
146
147    // Strip #[uuid(..)] attributes from the variants and set the discriminants.
148    for (variant, value) in new_enum
149        .variants
150        .iter_mut()
151        .zip(variant_values.iter().copied())
152    {
153        // Retain all attributes except #[uuid(..)] on the variant.
154        variant.attrs.retain(|attr| !is_uuid_attr(attr));
155
156        let lit = LitInt::new(&format!("{value}_u128"), variant.span());
157        variant.discriminant = Some((
158            syn::token::Eq {
159                spans: [variant.span()],
160            },
161            Expr::Lit(ExprLit {
162                attrs: Vec::new(),
163                lit: Lit::Int(lit),
164            }),
165        ));
166    }
167
168    // Build the impl block. We use fully-qualified core and uuid_enum paths so
169    // generated code is independent of user imports.
170    let expanded = {
171        let all_variants = &variant_idents;
172
173        quote! {
174            #new_enum
175
176            impl #ident {
177                /// All variants of this enum.
178                pub const ALL: &'static [Self] = &[
179                    #( Self::#all_variants, )*
180                ];
181
182                /// Convert this enum variant into its raw `u128` discriminant.
183                pub const fn as_u128(self) -> u128 {
184                    self as u128
185                }
186
187                /// Try to recover an enum variant from its raw `u128` discriminant.
188                pub const fn from_u128(raw: u128) -> ::core::option::Option<Self> {
189                    match raw {
190                        #(
191                            x if x == Self::#all_variants as u128 =>
192                                ::core::option::Option::Some(Self::#all_variants),
193                        )*
194                        _ => ::core::option::Option::None,
195                    }
196                }
197
198                /// Convert this enum variant into a `Uuid` from the `uuid_enum` façade crate.
199                pub const fn to_uuid(self) -> ::uuid_enum::uuid::Uuid {
200                    ::uuid_enum::uuid::Uuid::from_u128(self.as_u128())
201                }
202
203                /// Try to recover an enum variant from a `Uuid` value.
204                pub const fn from_uuid(id: ::uuid_enum::uuid::Uuid) -> ::core::option::Option<Self> {
205                    Self::from_u128(id.as_u128())
206                }
207            }
208        }
209    };
210
211    Ok(expanded.into())
212}
213
214fn ensure_unit_variant(variant: &Variant) -> syn::Result<()> {
215    if !variant.fields.is_empty() {
216        return Err(syn::Error::new(
217            variant.span(),
218            "#[uuid_enum] only supports C-like (fieldless) enum variants",
219        ));
220    }
221    Ok(())
222}
223
224fn is_uuid_attr(attr: &Attribute) -> bool {
225    attr.path().is_ident("uuid")
226}
227
228fn find_uuid_attribute<'a>(attrs: &'a [Attribute]) -> Option<&'a Attribute> {
229    attrs.iter().find(|a| is_uuid_attr(a))
230}
231
232/// Parse a `#[uuid("…")]` attribute into its string literal.
233fn parse_uuid_attribute(attr: &Attribute) -> syn::Result<String> {
234    // Accept either #[uuid("…")] or #[uuid = "…"].
235    match &attr.meta {
236        Meta::NameValue(MetaNameValue { value, .. }) => {
237            if let Expr::Lit(ExprLit {
238                lit: Lit::Str(s), ..
239            }) = value
240            {
241                return Ok(s.value());
242            } else {
243                return Err(syn::Error::new_spanned(
244                    value,
245                    "#[uuid = ...] expects a string literal",
246                ));
247            }
248        }
249        Meta::Path(_) => {
250            return Err(syn::Error::new_spanned(
251                attr,
252                "#[uuid] is missing the string literal value",
253            ))
254        }
255        Meta::List(_) => {
256            let s: LitStr = attr.parse_args()?;
257            return Ok(s.value());
258        }
259    }
260}