pikadick_slash_framework_derive/
lib.rs

1#![allow(clippy::uninlined_format_args)]
2
3use proc_macro2::TokenStream;
4use quote::{
5    quote,
6    quote_spanned,
7};
8use syn::{
9    parse_macro_input,
10    spanned::Spanned,
11    Data,
12    DeriveInput,
13    Error,
14    Fields,
15    LitStr,
16    Result,
17};
18
19#[proc_macro_derive(FromOptions, attributes(pikadick_slash_framework))]
20pub fn derive_from_options(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
21    let input = parse_macro_input!(input as DeriveInput);
22
23    let name = input.ident;
24    match extract_fields(&input.data) {
25        Ok(fields) => {
26            let from_options_impl =
27                gen_from_options_impl(&fields).unwrap_or_else(Error::into_compile_error);
28            let get_argument_params_impl =
29                gen_get_argument_params_impl(&fields).unwrap_or_else(Error::into_compile_error);
30
31            let expanded = quote! {
32                impl ::pikadick_slash_framework::FromOptions for #name {
33                    fn from_options(
34                        interaction: &::serenity::model::application::CommandInteraction
35                    ) -> ::std::result::Result<Self, ::pikadick_slash_framework::ConvertError> {
36                        #from_options_impl
37                    }
38
39                    fn get_argument_params() -> ::std::result::Result<::std::vec::Vec<::pikadick_slash_framework::ArgumentParam>, ::pikadick_slash_framework::BuilderError> {
40                        #get_argument_params_impl
41                    }
42                }
43            };
44
45            proc_macro::TokenStream::from(expanded)
46        }
47        Err(e) => Error::into_compile_error(e).into(),
48    }
49}
50
51/// Extract Field s from a derive object
52fn extract_fields(data: &syn::Data) -> Result<Vec<Field<'_>>> {
53    let fields = match data {
54        Data::Struct(data) => &data.fields,
55        Data::Enum(data) => {
56            return Err(Error::new(
57                data.enum_token.span(),
58                "enums are not supported",
59            ))
60        }
61        Data::Union(data) => {
62            return Err(Error::new(
63                data.union_token.span(),
64                "unions are not supported",
65            ))
66        }
67    };
68
69    // Only use named fields
70    let fields = match fields {
71        Fields::Named(fields) => fields,
72        Fields::Unnamed(fields) => {
73            return Err(Error::new(
74                fields.span(),
75                "unnamed fields are not supported",
76            ))
77        }
78        Fields::Unit => return Err(Error::new(fields.span(), "unit structs are not supported")),
79    };
80
81    fields
82        .named
83        .iter()
84        .map(|field| {
85            let ident = field
86                .ident
87                .as_ref()
88                .expect("named struct fields should have names for all fields");
89
90            let mut maybe_rename = None;
91            let mut maybe_description = None;
92
93            for attr in field
94                .attrs
95                .iter()
96                .filter(|field| matches!(field.style, syn::AttrStyle::Outer))
97            {
98                if attr.path().is_ident("doc") {
99                    // doc comments show up here.
100                    // TODO: Consider doing something with them
101                    continue;
102                }
103
104                if attr.path().is_ident("pikadick_slash_framework") {
105                    attr.parse_nested_meta(|meta| {
106                        if meta.path.is_ident("rename") {
107                            let value = meta.value()?;
108                            let value: syn::LitStr = value.parse()?;
109
110                            if maybe_rename.is_some() {
111                                return Err(Error::new(attr.span(), "duplicate rename attribute"));
112                            }
113
114                            // TODO: Consider validating name
115                            maybe_rename = Some((value.value(), value.span()));
116
117                            return Ok(());
118                        }
119
120                        if meta.path.is_ident("description") {
121                            let value = meta.value()?;
122                            let value: syn::LitStr = value.parse()?;
123
124                            if maybe_description.is_some() {
125                                return Err(Error::new(
126                                    attr.span(),
127                                    "duplicate description attribute",
128                                ));
129                            }
130
131                            // TODO: Consider validating description
132                            maybe_description = Some((value.value(), value.span()));
133
134                            return Ok(());
135                        }
136
137                        Err(Error::new(ident.span(), "unexpected ident"))
138                    })?;
139                }
140            }
141
142            Ok(Field {
143                ident,
144                span: field.span(),
145                ty: &field.ty,
146
147                rename: maybe_rename,
148                description: maybe_description,
149            })
150        })
151        .collect::<Result<Vec<Field>>>()
152}
153
154fn gen_from_options_impl(fields: &[Field]) -> Result<TokenStream> {
155    let optional_field_recurse = fields.iter().map(|field| {
156        let name = &field.ident;
157        quote_spanned! {field.span=>
158            let mut #name = ::std::option::Option::None;
159        }
160    });
161
162    let match_recurse = fields.iter().map(|field| {
163        let name = &field.ident;
164        let name_lit = field.get_name_literal();
165        let ty = &field.ty;
166        quote_spanned! {field.span=>
167            #name_lit => {
168                #name = Some(
169                    <#ty as ::pikadick_slash_framework::FromOptionValue>::from_option_value(
170                        #name_lit,
171                        &option.value
172                    )?
173                );
174            }
175        }
176    });
177
178    let unwrap_field_recurse = fields.iter().map(|field| {
179        let name = &field.ident;
180        let name_lit = field.get_name_literal();
181        let ty = &field.ty;
182        quote_spanned! {field.span=>
183            let #name = #name
184                .or_else(<#ty as ::pikadick_slash_framework::FromOptionValue>::get_missing_default)
185                .ok_or(::pikadick_slash_framework::ConvertError::MissingRequiredField {
186                    name: #name_lit,
187                    expected: <#ty as ::pikadick_slash_framework::FromOptionValue>::get_expected_data_type()
188                })?;
189        }
190    });
191
192    let recurse = fields.iter().map(|field| {
193        let name = &field.ident;
194        quote_spanned! {field.span=>
195            #name,
196        }
197    });
198
199    Ok(quote! {
200        #(#optional_field_recurse)*
201
202        for option in interaction.data.options.iter() {
203            match option.name.as_str() {
204                #(#match_recurse)*
205                _ => {}
206            }
207        }
208
209        #(#unwrap_field_recurse)*
210
211        Ok(Self { #(#recurse)* })
212    })
213}
214
215fn gen_get_argument_params_impl(fields: &[Field]) -> Result<TokenStream> {
216    let fields_len = fields.len();
217
218    let args = fields.iter().map(|field| {
219        let name_lit = field.get_name_literal();
220        let description = field.get_description();
221        let ty = &field.ty;
222        quote_spanned! {field.span=>
223            ret.push(
224                ::pikadick_slash_framework::ArgumentParamBuilder::new()
225                    .name(#name_lit)
226                    .description(#description)
227                    .kind(<#ty as ::pikadick_slash_framework::FromOptionValue>::get_expected_data_type())
228                    .build()?
229            );
230        }
231    });
232
233    Ok(quote! {
234        let mut ret = ::std::vec::Vec::with_capacity(#fields_len);
235        #(#args)*
236        Ok(ret)
237    })
238}
239
240struct Field<'a> {
241    ident: &'a proc_macro2::Ident,
242    span: proc_macro2::Span,
243    ty: &'a syn::Type,
244
245    /// The renamed name of this field
246    rename: Option<(String, proc_macro2::Span)>,
247
248    /// The field description.
249    ///
250    /// This is different from documentation.
251    description: Option<(String, proc_macro2::Span)>,
252}
253
254impl Field<'_> {
255    /// Get the string literal name of this field.
256    ///
257    /// This will take into account field renames
258    fn get_name_literal(&self) -> LitStr {
259        match &self.rename {
260            Some((name, span)) => LitStr::new(name, *span),
261            None => LitStr::new(&self.ident.to_string(), self.ident.span()),
262        }
263    }
264
265    /// Get the description of this field.
266    ///
267    /// This will autogenerate a description if one is missing.
268    fn get_description(&self) -> LitStr {
269        match &self.description {
270            Some((description, span)) => LitStr::new(description, *span),
271            None => LitStr::new(
272                &format!(
273                    "The `{}` parameter",
274                    self.rename
275                        .as_ref()
276                        .map(|t| t.0.to_string())
277                        .unwrap_or_else(|| self.ident.to_string())
278                ),
279                self.ident.span(),
280            ),
281        }
282    }
283}