uuid_enum_proc_macros/
lib.rs1use 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 static SEEN_UUIDS: RefCell<HashMap<u128, Span>> = RefCell::new(HashMap::new());
16}
17
18#[proc_macro_attribute]
47pub fn uuid_enum(attr: TokenStream, item: TokenStream) -> TokenStream {
48 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 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 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 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 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 let mut new_enum = item.clone();
145 new_enum.attrs.push(syn::parse_quote!(#[repr(u128)]));
146
147 for (variant, value) in new_enum
149 .variants
150 .iter_mut()
151 .zip(variant_values.iter().copied())
152 {
153 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 let expanded = {
171 let all_variants = &variant_idents;
172
173 quote! {
174 #new_enum
175
176 impl #ident {
177 pub const ALL: &'static [Self] = &[
179 #( Self::#all_variants, )*
180 ];
181
182 pub const fn as_u128(self) -> u128 {
184 self as u128
185 }
186
187 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 pub const fn to_uuid(self) -> ::uuid_enum::uuid::Uuid {
200 ::uuid_enum::uuid::Uuid::from_u128(self.as_u128())
201 }
202
203 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
232fn parse_uuid_attribute(attr: &Attribute) -> syn::Result<String> {
234 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}