Skip to main content

extendr_macros/
list_struct.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{Data, DeriveInput};
4
5/// Implementation of the TryFromList macro. Refer to the documentation there
6pub fn derive_try_from_list(item: TokenStream) -> syn::parse::Result<TokenStream> {
7    // Parse the tokens into a Struct
8    let ast = syn::parse::<DeriveInput>(item)?;
9    let inside = if let Data::Struct(inner) = ast.data {
10        inner
11    } else {
12        return Err(syn::Error::new_spanned(ast, "Only struct is supported"));
13    };
14    let struct_name = ast.ident;
15    let struct_name_str = struct_name.to_string();
16
17    // Iterate each struct field and capture a conversion from Robj for each field
18    let mut tokens = Vec::<_>::with_capacity(inside.fields.len());
19    let is_tuple_struct = inside
20        .fields
21        .iter()
22        .next()
23        .map(|x| x.ident.is_none())
24        .unwrap_or(false);
25    for (id_field, field) in inside.fields.iter().enumerate() {
26        if is_tuple_struct {
27            let field_name = syn::Index::from(id_field);
28            let field_str = format!(".{id_field}");
29            // This is like `value[[id_field]]` in R
30            tokens.push(quote!(
31                #field_name: value
32                    .elt(#id_field)?
33                    .try_into()
34                    .map_err(|error| extendr_api::error::Error::Other(format!(
35                        "failed to convert tuple field `{}` on `{}`: {}",
36                        #field_str,
37                        #struct_name_str,
38                        error
39                    )))?
40            ));
41        } else {
42            let field_name = field.ident.as_ref().unwrap();
43            let field_str = field_name.to_string();
44            // This is like `value$foo` in R
45            tokens.push(quote!(
46                #field_name: value
47                    .dollar(#field_str)?
48                    .try_into()
49                    .map_err(|error| extendr_api::error::Error::Other(format!(
50                        "failed to convert field `{}` on `{}`: {}",
51                        #field_str,
52                        #struct_name_str,
53                        error
54                    )))?
55            ));
56        }
57    }
58
59    // Emit the conversion trait impl
60    Ok(TokenStream::from(quote!(
61        impl std::convert::TryFrom<&extendr_api::Robj> for #struct_name {
62            type Error = extendr_api::Error;
63
64            fn try_from(value: &extendr_api::Robj) -> extendr_api::Result<Self> {
65                let value: List = value.try_into()?;
66                Ok(#struct_name {
67                    #(#tokens),*
68                })
69            }
70        }
71
72        impl std::convert::TryFrom<extendr_api::Robj> for #struct_name {
73            type Error = extendr_api::Error;
74
75            fn try_from(value: extendr_api::Robj) -> extendr_api::Result<Self> {
76                let value: List = value.try_into()?;
77                Ok(#struct_name {
78                    #(#tokens),*
79                })
80            }
81        }
82    )))
83}
84
85/// Implementation of the `IntoList` macro. Refer to the documentation there
86pub fn derive_into_list(item: TokenStream) -> syn::parse::Result<TokenStream> {
87    // Parse the tokens into a Struct
88    let ast = syn::parse::<DeriveInput>(item)?;
89    let inside = if let Data::Struct(inner) = ast.data {
90        inner
91    } else {
92        return Err(syn::Error::new_spanned(ast, "Only `struct` is supported"));
93    };
94    let struct_name = ast.ident;
95
96    // Iterate each struct field and capture a token that creates a KeyValue pair (tuple) for
97    // each field
98    let mut tokens = Vec::<_>::with_capacity(inside.fields.len());
99
100    for (id_field, field) in inside.fields.iter().enumerate() {
101        let mut ignore = false;
102
103        let field_attributes = &field.attrs;
104        for attrib in field_attributes {
105            if !attrib.path().is_ident("into_list") {
106                continue;
107            }
108            let ignore_flag: syn::Meta = attrib.parse_args()?;
109            match ignore_flag {
110                syn::Meta::Path(path) => {
111                    if path.is_ident("ignore") {
112                        ignore = true;
113                    }
114                }
115                _ => {
116                    return Err(syn::Error::new_spanned(
117                        ignore_flag,
118                        "unrecognized attribute for `IntoList`",
119                    ))
120                }
121            }
122        }
123
124        if ignore {
125            continue;
126        }
127
128        let is_tuple_struct = field.colon_token.is_none();
129        if is_tuple_struct {
130            let dot_field_name = format!(".{id_field}");
131            let id_field_index = syn::Index::from(id_field);
132            tokens.push(quote!(
133                (#dot_field_name, (&value.#id_field_index).into())
134            ));
135        } else {
136            let field_name = field.ident.as_ref().unwrap();
137            let field_str = field_name.to_string();
138            tokens.push(quote!(
139                (#field_str, (&value.#field_name).into())
140            ));
141        }
142    }
143
144    // The only thing we emit from this macro is the conversion trait impl
145    Ok(TokenStream::from(quote!(
146        impl std::convert::From<&#struct_name> for extendr_api::Robj {
147            fn from(value: &#struct_name) -> Self {
148                extendr_api::List::from_pairs([#(#tokens),*]).into()
149            }
150        }
151        impl std::convert::From<#struct_name> for extendr_api::Robj {
152            fn from(value: #struct_name) -> Self {
153                (&value).into()
154            }
155        }
156    )))
157}