use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use std::cell::RefCell;
use std::collections::HashMap;
use syn::{
parse_macro_input, spanned::Spanned, Attribute, Expr, ExprLit, ItemEnum, Lit, LitInt, LitStr,
Meta, MetaNameValue, Variant,
};
thread_local! {
static SEEN_UUIDS: RefCell<HashMap<u128, Span>> = RefCell::new(HashMap::new());
}
#[proc_macro_attribute]
pub fn uuid_enum(attr: TokenStream, item: TokenStream) -> TokenStream {
if !attr.is_empty() {
let ts = proc_macro2::TokenStream::from(attr);
return syn::Error::new_spanned(ts, "#[uuid_enum] does not take any arguments")
.to_compile_error()
.into();
}
let input = parse_macro_input!(item as ItemEnum);
match expand_uuid_enum(input) {
Ok(ts) => ts,
Err(e) => e.to_compile_error().into(),
}
}
fn expand_uuid_enum(item: ItemEnum) -> syn::Result<TokenStream> {
if !item.generics.params.is_empty() {
return Err(syn::Error::new_spanned(
&item.generics,
"#[uuid_enum] currently only supports non-generic enums",
));
}
for attr in &item.attrs {
if attr.path().is_ident("repr") {
return Err(syn::Error::new_spanned(
attr,
"#[uuid_enum] must not be combined with an explicit #[repr(..)] on the enum",
));
}
}
let ident = &item.ident;
let mut variant_idents = Vec::new();
let mut variant_values = Vec::new();
for variant in &item.variants {
ensure_unit_variant(variant)?;
if variant.discriminant.is_some() {
return Err(syn::Error::new_spanned(
variant,
"#[uuid_enum] does not allow explicit discriminants; they are derived from #[uuid(\"…\")]",
));
}
let uuid_attr = find_uuid_attribute(&variant.attrs).ok_or_else(|| {
syn::Error::new(
variant.span(),
"each variant in a #[uuid_enum] must have a #[uuid(\"…\")] attribute",
)
})?;
let uuid_str = parse_uuid_attribute(uuid_attr)?;
let uuid = uuid::Uuid::parse_str(&uuid_str).map_err(|e| {
syn::Error::new(
uuid_attr.span(),
format!("invalid UUID string in #[uuid(..)]: {e}"),
)
})?;
let value = uuid.as_u128();
let dup = SEEN_UUIDS.with(|cell| {
let mut map = cell.borrow_mut();
if let Some(prev_span) = map.get(&value) {
Some(*prev_span)
} else {
map.insert(value, uuid_attr.span());
None
}
});
if let Some(prev_span) = dup {
let mut err = syn::Error::new(
uuid_attr.span(),
"this UUID is already used elsewhere in this crate",
);
err.combine(syn::Error::new(
prev_span,
"previous use of this UUID is here",
));
return Err(err);
}
variant_idents.push(&variant.ident);
variant_values.push(value);
}
let mut new_enum = item.clone();
new_enum.attrs.push(syn::parse_quote!(#[repr(u128)]));
for (variant, value) in new_enum
.variants
.iter_mut()
.zip(variant_values.iter().copied())
{
variant.attrs.retain(|attr| !is_uuid_attr(attr));
let lit = LitInt::new(&format!("{value}_u128"), variant.span());
variant.discriminant = Some((
syn::token::Eq {
spans: [variant.span()],
},
Expr::Lit(ExprLit {
attrs: Vec::new(),
lit: Lit::Int(lit),
}),
));
}
let expanded = {
let all_variants = &variant_idents;
quote! {
#new_enum
impl #ident {
pub const ALL: &'static [Self] = &[
#( Self::#all_variants, )*
];
pub const fn as_u128(self) -> u128 {
self as u128
}
pub const fn from_u128(raw: u128) -> ::core::option::Option<Self> {
match raw {
#(
x if x == Self::#all_variants as u128 =>
::core::option::Option::Some(Self::#all_variants),
)*
_ => ::core::option::Option::None,
}
}
pub const fn to_uuid(self) -> ::uuid_enum::uuid::Uuid {
::uuid_enum::uuid::Uuid::from_u128(self.as_u128())
}
pub const fn from_uuid(id: ::uuid_enum::uuid::Uuid) -> ::core::option::Option<Self> {
Self::from_u128(id.as_u128())
}
}
}
};
Ok(expanded.into())
}
fn ensure_unit_variant(variant: &Variant) -> syn::Result<()> {
if !variant.fields.is_empty() {
return Err(syn::Error::new(
variant.span(),
"#[uuid_enum] only supports C-like (fieldless) enum variants",
));
}
Ok(())
}
fn is_uuid_attr(attr: &Attribute) -> bool {
attr.path().is_ident("uuid")
}
fn find_uuid_attribute<'a>(attrs: &'a [Attribute]) -> Option<&'a Attribute> {
attrs.iter().find(|a| is_uuid_attr(a))
}
fn parse_uuid_attribute(attr: &Attribute) -> syn::Result<String> {
match &attr.meta {
Meta::NameValue(MetaNameValue { value, .. }) => {
if let Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) = value
{
return Ok(s.value());
} else {
return Err(syn::Error::new_spanned(
value,
"#[uuid = ...] expects a string literal",
));
}
}
Meta::Path(_) => {
return Err(syn::Error::new_spanned(
attr,
"#[uuid] is missing the string literal value",
))
}
Meta::List(_) => {
let s: LitStr = attr.parse_args()?;
return Ok(s.value());
}
}
}