1struct StructEnumAttrs {
30 class: Option<String>,
32 base: Option<String>,
34 list: bool,
36 dataframe: bool,
38 externalptr: bool,
40 match_arg: bool,
42 factor: bool,
44 prefer: Option<String>,
47}
48
49fn parse_attrs(attr: proc_macro::TokenStream) -> syn::Result<StructEnumAttrs> {
56 use syn::parse::Parser;
57
58 let mut attrs = StructEnumAttrs {
59 class: None,
60 base: None,
61 list: false,
62 dataframe: false,
63 externalptr: false,
64 match_arg: false,
65 factor: false,
66 prefer: None,
67 };
68
69 let parser = syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated;
71 let metas = parser.parse(attr)?;
72
73 for meta in &metas {
74 match meta {
75 syn::Meta::Path(path) => {
76 let ident = path
77 .get_ident()
78 .ok_or_else(|| syn::Error::new_spanned(path, "expected identifier"))?;
79 match ident.to_string().as_str() {
80 "list" => attrs.list = true,
81 "dataframe" => attrs.dataframe = true,
82 "externalptr" => attrs.externalptr = true,
83 "match_arg" => attrs.match_arg = true,
84 "factor" => attrs.factor = true,
85 _ => {
86 return Err(syn::Error::new_spanned(
87 ident,
88 format!(
89 "unknown #[miniextendr] attribute `{}`; expected one of: \
90 list, dataframe, externalptr, match_arg, factor, prefer, class, base",
91 ident
92 ),
93 ));
94 }
95 }
96 }
97 syn::Meta::NameValue(nv) => {
98 let key = nv
99 .path
100 .get_ident()
101 .map(|i| i.to_string())
102 .unwrap_or_default();
103 if let syn::Expr::Lit(syn::ExprLit {
104 lit: syn::Lit::Str(s),
105 ..
106 }) = &nv.value
107 {
108 match key.as_str() {
109 "class" => attrs.class = Some(s.value()),
110 "base" => attrs.base = Some(s.value()),
111 "prefer" => attrs.prefer = Some(s.value()),
112 _ => {
113 return Err(syn::Error::new_spanned(
114 &nv.path,
115 format!(
116 "unknown #[miniextendr] attribute `{}`; expected one of: \
117 class, base, prefer",
118 key
119 ),
120 ));
121 }
122 }
123 }
124 }
125 syn::Meta::List(list) => {
126 return Err(syn::Error::new_spanned(
127 list,
128 "unexpected list-style attribute; use path (`list`) or key-value (`class = \"...\"`) syntax",
129 ));
130 }
131 }
132 }
133
134 Ok(attrs)
135}
136
137fn field_count(item: &syn::ItemStruct) -> usize {
139 match &item.fields {
140 syn::Fields::Named(f) => f.named.len(),
141 syn::Fields::Unnamed(f) => f.unnamed.len(),
142 syn::Fields::Unit => 0,
143 }
144}
145
146fn is_fieldless_enum(item: &syn::ItemEnum) -> bool {
148 item.variants
149 .iter()
150 .all(|v| matches!(v.fields, syn::Fields::Unit))
151}
152
153pub fn expand_struct_or_enum(
161 attr: proc_macro::TokenStream,
162 item: proc_macro::TokenStream,
163) -> proc_macro::TokenStream {
164 if let Ok(item_struct) = syn::parse::<syn::ItemStruct>(item.clone()) {
166 return expand_struct(attr, item, &item_struct);
167 }
168
169 if let Ok(item_enum) = syn::parse::<syn::ItemEnum>(item.clone()) {
170 return expand_enum(attr, item, &item_enum);
171 }
172
173 syn::Error::new(
175 proc_macro2::Span::call_site(),
176 "#[miniextendr] on non-function items requires a struct or enum",
177 )
178 .into_compile_error()
179 .into()
180}
181
182fn expand_struct(
191 attr: proc_macro::TokenStream,
192 item: proc_macro::TokenStream,
193 item_struct: &syn::ItemStruct,
194) -> proc_macro::TokenStream {
195 let attrs = match parse_attrs(attr.clone()) {
196 Ok(a) => a,
197 Err(e) => return e.into_compile_error().into(),
198 };
199
200 let n_fields = field_count(item_struct);
201 let has_altrep_attrs = attrs.class.is_some() || attrs.base.is_some();
202 let has_mode_attr = attrs.list || attrs.dataframe || attrs.externalptr;
203
204 let effective_list = attrs.list || (!has_mode_attr && attrs.prefer.as_deref() == Some("list"));
206 let effective_dataframe =
207 attrs.dataframe || (!has_mode_attr && attrs.prefer.as_deref() == Some("dataframe"));
208 let effective_externalptr =
209 attrs.externalptr || (!has_mode_attr && attrs.prefer.as_deref() == Some("externalptr"));
210 let effective_mode = effective_list || effective_dataframe || effective_externalptr;
211
212 if n_fields == 1 && has_altrep_attrs && !effective_mode {
215 return syn::Error::new(
216 item_struct.ident.span(),
217 "#[miniextendr] no longer supports ALTREP (class/base attributes). \
218 Use #[derive(miniextendr_api::Altrep)] with #[altrep(class = \"...\")] instead.",
219 )
220 .into_compile_error()
221 .into();
222 }
223
224 if has_altrep_attrs && effective_mode {
226 return syn::Error::new(
227 item_struct.ident.span(),
228 "cannot combine ALTREP attributes (class, base) with mode attributes (list, dataframe, externalptr)",
229 )
230 .into_compile_error()
231 .into();
232 }
233
234 let mode_count = [effective_list, effective_dataframe, effective_externalptr]
236 .iter()
237 .filter(|&&b| b)
238 .count();
239 if mode_count > 1 {
240 return syn::Error::new(
241 item_struct.ident.span(),
242 "only one of `list`, `dataframe`, `externalptr` can be specified",
243 )
244 .into_compile_error()
245 .into();
246 }
247
248 if let Some(ref prefer) = attrs.prefer
250 && !matches!(
251 prefer.as_str(),
252 "externalptr" | "list" | "dataframe" | "native"
253 )
254 {
255 return syn::Error::new(
256 item_struct.ident.span(),
257 format!(
258 "unknown prefer value `{}`; expected one of: externalptr, list, dataframe, native",
259 prefer
260 ),
261 )
262 .into_compile_error()
263 .into();
264 }
265
266 let derive_input: syn::DeriveInput = match syn::parse(item.clone()) {
268 Ok(d) => d,
269 Err(e) => return e.into_compile_error().into(),
270 };
271 let derive_input = strip_miniextendr_attrs(derive_input);
273
274 let item_ts: proc_macro2::TokenStream = item.into();
275
276 if effective_list {
277 let result = (|| -> syn::Result<proc_macro2::TokenStream> {
279 let into_list = crate::list_derive::derive_into_list(derive_input.clone())?;
280 let try_from_list = crate::list_derive::derive_try_from_list(derive_input.clone())?;
281 let prefer_list = crate::list_derive::derive_prefer_list(derive_input)?;
282 Ok(quote::quote! {
283 #item_ts
284 #into_list
285 #try_from_list
286 #prefer_list
287 })
288 })();
289 return result.unwrap_or_else(|e| e.into_compile_error()).into();
290 }
291
292 if effective_dataframe {
293 let result = (|| -> syn::Result<proc_macro2::TokenStream> {
296 let into_list = crate::list_derive::derive_into_list(derive_input.clone())?;
297 let dataframe_row = crate::dataframe_derive::derive_dataframe_row(derive_input)?;
298 Ok(quote::quote! {
299 #item_ts
300 #into_list
301 #dataframe_row
302 })
303 })();
304 return result.unwrap_or_else(|e| e.into_compile_error()).into();
305 }
306
307 let result = (|| -> syn::Result<proc_macro2::TokenStream> {
310 let external_ptr = crate::externalptr_derive::derive_external_ptr(derive_input.clone())?;
311
312 let prefer = if attrs.prefer.as_deref() == Some("native") {
314 crate::list_derive::derive_prefer_rnative(derive_input)?
315 } else {
316 proc_macro2::TokenStream::new()
317 };
318
319 Ok(quote::quote! {
320 #item_ts
321 #external_ptr
322 #prefer
323 })
324 })();
325 result.unwrap_or_else(|e| e.into_compile_error()).into()
326}
327
328fn expand_enum(
334 attr: proc_macro::TokenStream,
335 item: proc_macro::TokenStream,
336 item_enum: &syn::ItemEnum,
337) -> proc_macro::TokenStream {
338 let attrs = match parse_attrs(attr) {
339 Ok(a) => a,
340 Err(e) => return e.into_compile_error().into(),
341 };
342
343 if !is_fieldless_enum(item_enum) {
344 return syn::Error::new(
345 item_enum.ident.span(),
346 "#[miniextendr] on enums requires all variants to be fieldless (C-style)",
347 )
348 .into_compile_error()
349 .into();
350 }
351
352 let derive_input: syn::DeriveInput = match syn::parse(item.clone()) {
353 Ok(d) => d,
354 Err(e) => return e.into_compile_error().into(),
355 };
356 let derive_input = strip_miniextendr_attrs(derive_input);
357
358 let item_ts: proc_macro2::TokenStream = item.into();
359
360 if attrs.match_arg {
361 let result = crate::match_arg_derive::derive_match_arg(derive_input);
363 return match result {
364 Ok(ts) => quote::quote! { #item_ts #ts }.into(),
365 Err(e) => e.into_compile_error().into(),
366 };
367 }
368
369 if attrs.factor {
370 let result = crate::factor_derive::derive_r_factor(derive_input);
372 return match result {
373 Ok(ts) => quote::quote! { #item_ts #ts }.into(),
374 Err(e) => e.into_compile_error().into(),
375 };
376 }
377
378 let result = crate::factor_derive::derive_r_factor(derive_input);
380 match result {
381 Ok(ts) => quote::quote! { #item_ts #ts }.into(),
382 Err(e) => e.into_compile_error().into(),
383 }
384}
385
386fn strip_miniextendr_attrs(mut input: syn::DeriveInput) -> syn::DeriveInput {
391 input
392 .attrs
393 .retain(|attr| !attr.path().is_ident("miniextendr"));
394 input
395}