1#![expect(clippy::doc_markdown)]
2#![expect(clippy::result_large_err)]
3#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
4use unsynn::{
5 BraceGroupContaining, BracketGroupContaining, CommaDelimitedVec, Cons, Either, Except, Gt,
6 Ident, LiteralString, Lt, Many, Optional, ParenthesisGroupContaining, Parse as _, PathSep,
7 PathSepDelimited, Pound, ToTokens as _, TokenStream, TokenTree, format_ident, quote, unsynn,
8};
9
10type ModPath = Cons<Option<PathSep>, PathSepDelimited<Ident>>;
13
14unsynn! {
15 operator Eq = "=";
16 keyword EnumKeyword = "enum";
17 keyword DocKeyword = "doc";
18 keyword ReprKeyword = "repr";
19 keyword PubKeyword = "pub";
20 keyword InKeyword = "in";
21 keyword ConstKeyword = "const";
22
23 struct DocInner {
25 _kw_doc: DocKeyword,
27 _eq: Eq,
29 value: LiteralString,
31 }
32
33 struct ReprInner {
36 _kw_repr: ReprKeyword,
38 attr: ParenthesisGroupContaining<CommaDelimitedVec<Ident>>,
40 }
41
42 enum AttributeInner {
44 Doc(DocInner),
46 Repr(ReprInner),
48 Any(Many<TokenTree>),
50 }
51
52 struct Attribute {
54 _pound: Pound,
56 body: BracketGroupContaining<AttributeInner>,
58 }
59
60 enum Vis {
62 PubIn(Cons<PubKeyword, ParenthesisGroupContaining<Cons<Option<InKeyword>, ModPath>>>),
64 Pub(PubKeyword),
66 }
67
68 struct AngleTokenTree(
71 pub Either<Cons<Lt, Many<Cons<Except<Gt>, AngleTokenTree>>, Gt>, TokenTree>
72 );
73
74 struct Type{
76 pub name: Ident,
77 pub generics: Optional<AngleTokenTree>,
78 }
79
80 struct EnumVariant {
82 name: Ident,
84 body: ParenthesisGroupContaining<Type>,
86 }
87
88 struct SimpleEnum {
90 _attributes: Optional<Many<Attribute>>,
92 _vis: Optional<Vis>,
94 _enum_token: EnumKeyword,
96 name: Ident,
98 body: BraceGroupContaining<CommaDelimitedVec<EnumVariant>>,
100 }
101}
102
103#[proc_macro_derive(AsToVariant)]
106pub fn derive_as_to_variant(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
107 let input: TokenStream = input.into();
108 let mut it = input.to_token_iter();
109
110 let enum_def = match SimpleEnum::parse(&mut it) {
111 Ok(def) => def,
112 Err(e) => panic!("failed to parse enum definition: {e:#?}"),
113 };
114
115 let enum_name = enum_def.name;
116 let variants = enum_def.body.content;
117
118 let variant_methods = variants.into_iter().map(|variant| {
120 let variant_name = &variant.value.name;
121 let variant_name_snake = to_snake_case(&variant.value.name.to_string());
122 let to_method = format_ident!("to_{variant_name_snake}");
123 let as_method = format_ident!("as_{variant_name_snake}");
124 let inner_type = variant.value.body.content.into_token_stream();
125 let doc_to = LiteralString::from_str(format!(
126 "Convert to the inner {variant_name_snake} definition."
127 ));
128 let doc_as = LiteralString::from_str(format!(
129 "Reference to the inner {variant_name_snake} definition."
130 ));
131
132 quote! {
133 #[doc = #doc_to]
134 #[must_use]
135 pub fn #to_method(self) -> Option<#inner_type> {
136 match self {
137 #enum_name::#variant_name(value) => Some(value),
138 _ => None,
139 }
140 }
141
142 #[doc = #doc_as]
143 #[must_use]
144 pub fn #as_method(&self) -> Option<&#inner_type> {
145 match self {
146 #enum_name::#variant_name(value) => Some(value),
147 _ => None,
148 }
149 }
150 }
151 });
152
153 let expanded = quote! {
154 impl #enum_name {
155 #{variant_methods}
156 }
157 };
158
159 proc_macro::TokenStream::from(expanded)
160}
161
162fn to_snake_case(input: &str) -> String {
164 let words = split_into_words(input);
165 words
166 .iter()
167 .map(|word| word.to_lowercase())
168 .collect::<Vec<_>>()
169 .join("_")
170}
171
172fn split_into_words(input: &str) -> Vec<String> {
182 if input.is_empty() {
183 return vec![];
184 }
185
186 let mut words = Vec::new();
187 let mut current_word = String::new();
188 let mut chars = input.chars().peekable();
189
190 while let Some(c) = chars.next() {
191 if c == '_' || c == '-' || c.is_whitespace() {
193 if !current_word.is_empty() {
194 words.push(std::mem::take(&mut current_word));
195 }
196 continue;
197 }
198
199 let next = chars.peek().copied();
201
202 if c.is_uppercase() {
203 if let Some(prev) = current_word.chars().last() {
204 if prev.is_lowercase()
209 || prev.is_ascii_digit()
210 || (prev.is_uppercase() && next.is_some_and(char::is_lowercase))
211 {
212 words.push(std::mem::take(&mut current_word));
213 }
214 }
215 current_word.push(c);
216 } else {
217 current_word.push(c);
220 }
221 }
222
223 if !current_word.is_empty() {
224 words.push(current_word);
225 }
226
227 words.into_iter().filter(|s| !s.is_empty()).collect()
228}