miniextendr_macros/
match_arg_derive.rs1use proc_macro2::TokenStream;
40use quote::quote;
41use syn::{Data, DeriveInput, Fields};
42
43use crate::naming::apply_rename_all;
44
45#[derive(Default)]
47struct MatchArgAttrs {
48 rename: Option<String>,
50 rename_all: Option<String>,
53}
54
55fn parse_match_arg_attrs(attrs: &[syn::Attribute]) -> syn::Result<MatchArgAttrs> {
61 let mut result = MatchArgAttrs::default();
62
63 for attr in attrs {
64 if attr.path().is_ident("match_arg") {
65 attr.parse_nested_meta(|meta| {
66 if meta.path.is_ident("rename") {
67 let value: syn::LitStr = meta.value()?.parse()?;
68 result.rename = Some(value.value());
69 } else if meta.path.is_ident("rename_all") {
70 let value: syn::LitStr = meta.value()?.parse()?;
71 let val = value.value();
72 match val.as_str() {
73 "snake_case" | "kebab-case" | "lower" | "upper" => {}
74 _ => {
75 return Err(meta.error(
76 "unsupported rename_all value; expected one of: \
77 snake_case, kebab-case, lower, upper",
78 ));
79 }
80 }
81 result.rename_all = Some(val);
82 } else {
83 return Err(meta
84 .error("unknown match_arg attribute; expected `rename` or `rename_all`"));
85 }
86 Ok(())
87 })?;
88 }
89 }
90
91 Ok(result)
92}
93
94pub fn derive_match_arg(input: DeriveInput) -> syn::Result<TokenStream> {
117 let name = &input.ident;
118 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
119
120 if !input.generics.params.is_empty() {
122 return Err(syn::Error::new_spanned(
123 &input.generics,
124 "#[derive(MatchArg)] does not support generic enums",
125 ));
126 }
127
128 let attrs = parse_match_arg_attrs(&input.attrs)?;
130
131 let variants = match &input.data {
133 Data::Enum(data) => &data.variants,
134 Data::Struct(_) => {
135 return Err(syn::Error::new_spanned(
136 &input,
137 "#[derive(MatchArg)] can only be applied to enums",
138 ));
139 }
140 Data::Union(_) => {
141 return Err(syn::Error::new_spanned(
142 &input,
143 "#[derive(MatchArg)] can only be applied to enums",
144 ));
145 }
146 };
147
148 if variants.is_empty() {
149 return Err(syn::Error::new_spanned(
150 &input,
151 "#[derive(MatchArg)] requires at least one variant",
152 ));
153 }
154
155 let mut choice_names = Vec::new();
156 let mut variant_idents = Vec::new();
157
158 for variant in variants {
159 if !matches!(variant.fields, Fields::Unit) {
161 return Err(syn::Error::new_spanned(
162 variant,
163 "#[derive(MatchArg)] only supports fieldless (C-style) enum variants",
164 ));
165 }
166
167 let var_attrs = parse_match_arg_attrs(&variant.attrs)?;
169
170 let choice_name = if let Some(r) = var_attrs.rename {
172 r
173 } else {
174 apply_rename_all(&variant.ident.to_string(), attrs.rename_all.as_deref())
175 };
176
177 choice_names.push(choice_name);
178 variant_idents.push(&variant.ident);
179 }
180
181 {
183 let mut seen = std::collections::HashSet::new();
184 for (i, name) in choice_names.iter().enumerate() {
185 if !seen.insert(name.as_str()) {
186 return Err(syn::Error::new_spanned(
187 &variants.iter().nth(i).unwrap().ident,
188 format!("duplicate choice name {:?} in #[derive(MatchArg)]", name),
189 ));
190 }
191 }
192 }
193
194 let choice_strs: Vec<&str> = choice_names.iter().map(|s| s.as_str()).collect();
195
196 Ok(quote! {
197 impl #impl_generics ::miniextendr_api::match_arg::MatchArg for #name #ty_generics #where_clause {
198 const CHOICES: &'static [&'static str] = &[#(#choice_strs),*];
199
200 fn from_choice(choice: &str) -> Option<Self> {
201 match choice {
202 #(#choice_strs => Some(Self::#variant_idents),)*
203 _ => None,
204 }
205 }
206
207 fn to_choice(self) -> &'static str {
208 match self {
209 #(Self::#variant_idents => #choice_strs,)*
210 }
211 }
212 }
213
214 impl #impl_generics ::miniextendr_api::TryFromSexp for #name #ty_generics #where_clause {
215 type Error = ::miniextendr_api::SexpError;
216
217 fn try_from_sexp(sexp: ::miniextendr_api::SEXP) -> Result<Self, Self::Error> {
218 ::miniextendr_api::match_arg_from_sexp(sexp).map_err(Into::into)
219 }
220 }
221
222 impl #impl_generics ::miniextendr_api::IntoR for #name #ty_generics #where_clause {
223 type Error = std::convert::Infallible;
224
225 fn try_into_sexp(self) -> Result<::miniextendr_api::SEXP, Self::Error> {
226 Ok(self.into_sexp())
227 }
228
229 unsafe fn try_into_sexp_unchecked(self) -> Result<::miniextendr_api::SEXP, Self::Error> {
230 self.try_into_sexp()
231 }
232
233 fn into_sexp(self) -> ::miniextendr_api::SEXP {
234 use ::miniextendr_api::match_arg::MatchArg;
235 self.to_choice().into_sexp()
236 }
237 }
238
239
240 })
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn test_simple_derive() {
249 let input: DeriveInput = syn::parse_quote! {
250 enum Mode {
251 Fast,
252 Safe,
253 Debug,
254 }
255 };
256
257 let result = derive_match_arg(input).unwrap();
258 let code = result.to_string();
259 assert!(code.contains("Fast"));
260 assert!(code.contains("Safe"));
261 assert!(code.contains("Debug"));
262 assert!(code.contains("CHOICES"));
263 assert!(code.contains("from_choice"));
264 assert!(code.contains("to_choice"));
265 }
266
267 #[test]
268 fn test_rename_all() {
269 let input: DeriveInput = syn::parse_quote! {
270 #[match_arg(rename_all = "snake_case")]
271 enum Mode {
272 FastMode,
273 SafeMode,
274 }
275 };
276
277 let result = derive_match_arg(input).unwrap();
278 let code = result.to_string();
279 assert!(code.contains("fast_mode"));
280 assert!(code.contains("safe_mode"));
281 }
282
283 #[test]
284 fn test_rename_variant() {
285 let input: DeriveInput = syn::parse_quote! {
286 enum Priority {
287 #[match_arg(rename = "lo")]
288 Low,
289 #[match_arg(rename = "hi")]
290 High,
291 }
292 };
293
294 let result = derive_match_arg(input).unwrap();
295 let code = result.to_string();
296 assert!(code.contains("\"lo\""));
297 assert!(code.contains("\"hi\""));
298 }
299
300 #[test]
301 fn test_reject_fields() {
302 let input: DeriveInput = syn::parse_quote! {
303 enum Bad {
304 A(i32),
305 }
306 };
307
308 let result = derive_match_arg(input);
309 assert!(result.is_err());
310 }
311
312 #[test]
313 fn test_reject_struct() {
314 let input: DeriveInput = syn::parse_quote! {
315 struct Bad;
316 };
317
318 let result = derive_match_arg(input);
319 assert!(result.is_err());
320 }
321
322 #[test]
323 fn test_reject_empty() {
324 let input: DeriveInput = syn::parse_quote! {
325 enum Empty {}
326 };
327
328 let result = derive_match_arg(input);
329 assert!(result.is_err());
330 }
331
332 #[test]
333 fn test_into_r_impl_present() {
334 let input: DeriveInput = syn::parse_quote! {
338 enum Mode {
339 Fast,
340 Safe,
341 Debug,
342 }
343 };
344
345 let result = derive_match_arg(input).unwrap();
346 let code = result.to_string();
347 assert!(code.contains("IntoR for Mode"));
349 assert!(!code.contains("IntoR for :: std :: vec :: Vec < Mode >"));
351 assert!(!code.contains("match_arg_vec_into_sexp"));
352 }
353
354 #[test]
355 fn test_duplicate_choice_names() {
356 let input: DeriveInput = syn::parse_quote! {
357 enum Dup {
358 #[match_arg(rename = "same")]
359 A,
360 #[match_arg(rename = "same")]
361 B,
362 }
363 };
364
365 let result = derive_match_arg(input);
366 assert!(result.is_err());
367 }
368}