#![expect(clippy::doc_markdown)]
#![expect(clippy::result_large_err)]
#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
use unsynn::{
BraceGroupContaining, BracketGroupContaining, CommaDelimitedVec, Cons, Either, Except, Gt,
Ident, LiteralString, Lt, Many, Optional, ParenthesisGroupContaining, Parse as _, PathSep,
PathSepDelimited, Pound, ToTokens as _, TokenStream, TokenTree, format_ident, quote, unsynn,
};
type ModPath = Cons<Option<PathSep>, PathSepDelimited<Ident>>;
unsynn! {
operator Eq = "=";
keyword EnumKeyword = "enum";
keyword DocKeyword = "doc";
keyword ReprKeyword = "repr";
keyword PubKeyword = "pub";
keyword InKeyword = "in";
keyword ConstKeyword = "const";
struct DocInner {
_kw_doc: DocKeyword,
_eq: Eq,
value: LiteralString,
}
struct ReprInner {
_kw_repr: ReprKeyword,
attr: ParenthesisGroupContaining<CommaDelimitedVec<Ident>>,
}
enum AttributeInner {
Doc(DocInner),
Repr(ReprInner),
Any(Many<TokenTree>),
}
struct Attribute {
_pound: Pound,
body: BracketGroupContaining<AttributeInner>,
}
enum Vis {
PubIn(Cons<PubKeyword, ParenthesisGroupContaining<Cons<Option<InKeyword>, ModPath>>>),
Pub(PubKeyword),
}
struct AngleTokenTree(
pub Either<Cons<Lt, Many<Cons<Except<Gt>, AngleTokenTree>>, Gt>, TokenTree>
);
struct Type{
pub name: Ident,
pub generics: Optional<AngleTokenTree>,
}
struct EnumVariant {
name: Ident,
body: ParenthesisGroupContaining<Type>,
}
struct SimpleEnum {
_attributes: Optional<Many<Attribute>>,
_vis: Optional<Vis>,
_enum_token: EnumKeyword,
name: Ident,
body: BraceGroupContaining<CommaDelimitedVec<EnumVariant>>,
}
}
#[proc_macro_derive(AsToVariant)]
pub fn derive_as_to_variant(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input: TokenStream = input.into();
let mut it = input.to_token_iter();
let enum_def = match SimpleEnum::parse(&mut it) {
Ok(def) => def,
Err(e) => panic!("failed to parse enum definition: {e:#?}"),
};
let enum_name = enum_def.name;
let variants = enum_def.body.content;
let variant_methods = variants.into_iter().map(|variant| {
let variant_name = &variant.value.name;
let variant_name_snake = to_snake_case(&variant.value.name.to_string());
let to_method = format_ident!("to_{variant_name_snake}");
let as_method = format_ident!("as_{variant_name_snake}");
let inner_type = variant.value.body.content.into_token_stream();
let doc_to = LiteralString::from_str(format!(
"Convert to the inner {variant_name_snake} definition."
));
let doc_as = LiteralString::from_str(format!(
"Reference to the inner {variant_name_snake} definition."
));
quote! {
#[doc = #doc_to]
#[must_use]
pub fn #to_method(self) -> Option<#inner_type> {
match self {
#enum_name::#variant_name(value) => Some(value),
_ => None,
}
}
#[doc = #doc_as]
#[must_use]
pub fn #as_method(&self) -> Option<&#inner_type> {
match self {
#enum_name::#variant_name(value) => Some(value),
_ => None,
}
}
}
});
let expanded = quote! {
impl #enum_name {
#{variant_methods}
}
};
proc_macro::TokenStream::from(expanded)
}
fn to_snake_case(input: &str) -> String {
let words = split_into_words(input);
words
.iter()
.map(|word| word.to_lowercase())
.collect::<Vec<_>>()
.join("_")
}
fn split_into_words(input: &str) -> Vec<String> {
if input.is_empty() {
return vec![];
}
let mut words = Vec::new();
let mut current_word = String::new();
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
if c == '_' || c == '-' || c.is_whitespace() {
if !current_word.is_empty() {
words.push(std::mem::take(&mut current_word));
}
continue;
}
let next = chars.peek().copied();
if c.is_uppercase() {
if let Some(prev) = current_word.chars().last() {
if prev.is_lowercase()
|| prev.is_ascii_digit()
|| (prev.is_uppercase() && next.is_some_and(char::is_lowercase))
{
words.push(std::mem::take(&mut current_word));
}
}
current_word.push(c);
} else {
current_word.push(c);
}
}
if !current_word.is_empty() {
words.push(current_word);
}
words.into_iter().filter(|s| !s.is_empty()).collect()
}