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(
194 attr: proc_macro::TokenStream,
195 item: proc_macro::TokenStream,
196 item_struct: &syn::ItemStruct,
197) -> proc_macro::TokenStream {
198 let attrs = match parse_attrs(attr.clone()) {
199 Ok(a) => a,
200 Err(e) => return e.into_compile_error().into(),
201 };
202
203 let n_fields = field_count(item_struct);
204 let has_altrep_attrs = attrs.class.is_some() || attrs.base.is_some();
205 let has_mode_attr = attrs.list || attrs.dataframe || attrs.externalptr;
206
207 let effective_list = attrs.list || (!has_mode_attr && attrs.prefer.as_deref() == Some("list"));
209 let effective_dataframe =
210 attrs.dataframe || (!has_mode_attr && attrs.prefer.as_deref() == Some("dataframe"));
211 let effective_externalptr =
212 attrs.externalptr || (!has_mode_attr && attrs.prefer.as_deref() == Some("externalptr"));
213 let effective_mode = effective_list || effective_dataframe || effective_externalptr;
214
215 if n_fields == 1 && has_altrep_attrs && !effective_mode {
218 return syn::Error::new(
219 item_struct.ident.span(),
220 "#[miniextendr] no longer supports ALTREP (class/base attributes). \
221 Use #[derive(miniextendr_api::Altrep)] with #[altrep(class = \"...\")] instead.",
222 )
223 .into_compile_error()
224 .into();
225 }
226
227 if has_altrep_attrs && effective_mode {
229 return syn::Error::new(
230 item_struct.ident.span(),
231 "cannot combine ALTREP attributes (class, base) with mode attributes (list, dataframe, externalptr)",
232 )
233 .into_compile_error()
234 .into();
235 }
236
237 let mode_count = [effective_list, effective_dataframe, effective_externalptr]
239 .iter()
240 .filter(|&&b| b)
241 .count();
242 if mode_count > 1 {
243 return syn::Error::new(
244 item_struct.ident.span(),
245 "only one of `list`, `dataframe`, `externalptr` can be specified",
246 )
247 .into_compile_error()
248 .into();
249 }
250
251 if let Some(ref prefer) = attrs.prefer
253 && !matches!(
254 prefer.as_str(),
255 "externalptr" | "list" | "dataframe" | "native"
256 )
257 {
258 return syn::Error::new(
259 item_struct.ident.span(),
260 format!(
261 "unknown prefer value `{}`; expected one of: externalptr, list, dataframe, native",
262 prefer
263 ),
264 )
265 .into_compile_error()
266 .into();
267 }
268
269 let derive_input: syn::DeriveInput = match syn::parse(item.clone()) {
271 Ok(d) => d,
272 Err(e) => return e.into_compile_error().into(),
273 };
274 let derive_input = strip_miniextendr_attrs(derive_input);
276
277 let item_ts: proc_macro2::TokenStream = item.into();
278
279 if effective_list {
280 let result = (|| -> syn::Result<proc_macro2::TokenStream> {
282 let into_list = crate::list_derive::derive_into_list(derive_input.clone())?;
283 let try_from_list = crate::list_derive::derive_try_from_list(derive_input.clone())?;
284 let prefer_list = crate::list_derive::derive_prefer_list(derive_input)?;
285 Ok(quote::quote! {
286 #item_ts
287 #into_list
288 #try_from_list
289 #prefer_list
290 })
291 })();
292 return result.unwrap_or_else(|e| e.into_compile_error()).into();
293 }
294
295 if effective_dataframe {
296 let result = (|| -> syn::Result<proc_macro2::TokenStream> {
299 let into_list = crate::list_derive::derive_into_list(derive_input.clone())?;
300 let dataframe_row = crate::dataframe_derive::derive_dataframe_row(derive_input)?;
301 Ok(quote::quote! {
302 #item_ts
303 #into_list
304 #dataframe_row
305 })
306 })();
307 return result.unwrap_or_else(|e| e.into_compile_error()).into();
308 }
309
310 let result = (|| -> syn::Result<proc_macro2::TokenStream> {
313 let external_ptr = crate::externalptr_derive::derive_external_ptr(
317 derive_input.clone(),
318 attrs.prefer.as_deref() != Some("native"),
319 )?;
320
321 let prefer = if attrs.prefer.as_deref() == Some("native") {
323 crate::list_derive::derive_prefer_rnative(derive_input)?
324 } else {
325 proc_macro2::TokenStream::new()
326 };
327
328 Ok(quote::quote! {
329 #item_ts
330 #external_ptr
331 #prefer
332 })
333 })();
334 result.unwrap_or_else(|e| e.into_compile_error()).into()
335}
336
337fn expand_enum(
343 attr: proc_macro::TokenStream,
344 item: proc_macro::TokenStream,
345 item_enum: &syn::ItemEnum,
346) -> proc_macro::TokenStream {
347 let attrs = match parse_attrs(attr) {
348 Ok(a) => a,
349 Err(e) => return e.into_compile_error().into(),
350 };
351
352 if !is_fieldless_enum(item_enum) {
353 return syn::Error::new(
354 item_enum.ident.span(),
355 "#[miniextendr] on enums requires all variants to be fieldless (C-style)",
356 )
357 .into_compile_error()
358 .into();
359 }
360
361 let derive_input: syn::DeriveInput = match syn::parse(item.clone()) {
362 Ok(d) => d,
363 Err(e) => return e.into_compile_error().into(),
364 };
365 let derive_input = strip_miniextendr_attrs(derive_input);
366
367 let item_ts: proc_macro2::TokenStream = item.into();
368
369 if attrs.match_arg {
370 let result = crate::match_arg_derive::derive_match_arg(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 if attrs.factor {
379 let result = crate::factor_derive::derive_r_factor(derive_input);
381 return match result {
382 Ok(ts) => quote::quote! { #item_ts #ts }.into(),
383 Err(e) => e.into_compile_error().into(),
384 };
385 }
386
387 let result = crate::factor_derive::derive_r_factor(derive_input);
389 match result {
390 Ok(ts) => quote::quote! { #item_ts #ts }.into(),
391 Err(e) => e.into_compile_error().into(),
392 }
393}
394
395fn strip_miniextendr_attrs(mut input: syn::DeriveInput) -> syn::DeriveInput {
400 input
401 .attrs
402 .retain(|attr| !attr.path().is_ident("miniextendr"));
403 input
404}