pikadick_slash_framework_derive/
lib.rs1#![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
51fn 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 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 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 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 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 rename: Option<(String, proc_macro2::Span)>,
247
248 description: Option<(String, proc_macro2::Span)>,
252}
253
254impl Field<'_> {
255 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 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}