tappet_derive/
lib.rs

1use proc_macro::TokenStream;
2
3use proc_macro2::Span;
4use quote::quote;
5use syn::punctuated::Punctuated;
6use syn::{parse_macro_input, Data, DeriveInput, Field, Fields, Ident, Meta};
7
8const META_COMMA: &str = "comma";
9const META_INDEXED: &str = "indexed";
10
11/// To retrieve the original name of the endpoint, we subtract the length of the word "Parameters".
12const PARAMETERS_LITERAL_LENGTH: usize = 10;
13
14type SimpleFields = Punctuated<syn::Field, syn::token::Comma>;
15
16#[derive(Debug, PartialEq)]
17enum VecMethod {
18    Comma,
19    Indexed,
20    None,
21}
22
23#[proc_macro_attribute]
24pub fn interface(attr: TokenStream, item: TokenStream) -> TokenStream {
25    let input = parse_macro_input!(item as DeriveInput);
26    let attribute = attr.to_string();
27
28    let name = &input.ident;
29    let my_ident = name.to_string();
30    let new_ident = Ident::new(
31        &my_ident[..my_ident.len() - PARAMETERS_LITERAL_LENGTH],
32        Span::call_site(),
33    );
34    let new_ident_stringified = new_ident.to_string();
35
36    let steam_interface_name = Ident::new(&attribute, Span::call_site());
37    let struct_fields = get_fields(&input).unwrap();
38    let field_idents: Vec<_> = struct_fields.iter().filter_map(|field| field.ident.as_ref()).collect();
39    let field_types: Vec<_> = struct_fields.iter().map(|field| &field.ty).collect();
40
41    let impl_convert = quote! {
42          #input
43
44        impl<'a> #steam_interface_name<'a> {
45            #[doc = "A new request to the `"]
46            #[doc = #new_ident_stringified]
47            #[doc = "` endpoint.\n\n"]
48            ///
49            /// The returning struct implements:
50            /// - `execute`, for a raw `String` response; Requires import of trait `Executor`;
51            /// - `execute_with_response`, for a jsonified response into a struct. **Needs to be available for this endpoint**. Requires import of trait `ExecutorResponse`;
52            /// - `inject_custom_key`, for injecting a custom api key different than the one used for instantiating `SteamAPI`, which
53            /// then you can execute with the options above.
54             pub fn #new_ident(self, #(#field_idents: #field_types),* ) -> #new_ident<'a> {
55                 let mut into: #new_ident = self.into();
56                 #(into.parameters.#field_idents = #field_idents;)*
57                 into
58             }
59        }
60    };
61
62    impl_convert.into()
63}
64
65#[proc_macro_derive(Parameters, attributes(comma, indexed))]
66pub fn derive_parameters(input: TokenStream) -> TokenStream {
67    // Parse the input tokens into a syntax tree
68    let input = parse_macro_input!(input as DeriveInput);
69
70    let struct_parameters_name = &input.ident;
71    let ident_str = struct_parameters_name.to_string();
72    let struct_fields = get_fields(&input).unwrap();
73    let processed_fields = process_fields(struct_fields);
74
75    let new_ident = Ident::new(
76        &ident_str[..ident_str.len() - PARAMETERS_LITERAL_LENGTH],
77        Span::call_site(),
78    );
79
80    // Build the output, possibly using quasi-quotation
81    let expanded = quote! {
82
83        #[derive(Debug)]
84        #[cfg(feature = "async")]
85        pub struct #new_ident<'a> {
86            pub(crate) key: &'a str,
87            pub(crate) request: reqwest::Request,
88            pub(crate) client: &'a reqwest::Client,
89            pub(crate) parameters: #struct_parameters_name,
90        }
91
92        #[derive(Debug)]
93        #[cfg(feature = "blocking")]
94        pub struct #new_ident<'a> {
95            pub(crate) key: &'a str,
96            pub(crate) request: reqwest::blocking::Request,
97            pub(crate) client: &'a reqwest::blocking::Client,
98            pub(crate) parameters: #struct_parameters_name,
99        }
100
101        impl<'a> #new_ident<'a> {
102            pub(crate) fn recover_params(&self) -> String {
103                self.parameters.recover_params()
104            }
105
106            pub(crate) fn recover_params_as_form(&self) -> &#struct_parameters_name {
107                &self.parameters
108            }
109
110            pub fn inject_custom_key(self, apikey: &'a str) -> Self {
111                let mut endpoint = self;
112                endpoint.key = apikey;
113                endpoint
114            }
115        }
116
117        impl #struct_parameters_name {
118                pub(crate) fn recover_params(&self) -> String {
119                    let mut query = String::new();
120                    #(#processed_fields)*;
121                    query
122                }
123        }
124    };
125
126    // Hand the output tokens back to the compiler
127    TokenStream::from(expanded)
128}
129
130fn process_fields(fields: &SimpleFields) -> Vec<proc_macro2::TokenStream> {
131    let mut tokens = Vec::new();
132
133    for field in fields {
134        let field_ident = &field.ident.as_ref().unwrap();
135        let field_ident_as_str = &field.ident.as_ref().unwrap().to_string();
136
137        let field_has_meta = is_comma_or_indexed(&field);
138        let is_option = is_option(&field.ty);
139
140        let vec_fn = match field_has_meta {
141            VecMethod::Comma => quote! {
142                &*querify(#field_ident_as_str, &comma_delimited(&#field_ident))
143            },
144            VecMethod::Indexed => quote! {
145               &indexed_array(#field_ident_as_str, &#field_ident)
146            },
147            VecMethod::None => quote! { &*querify(#field_ident_as_str, &#field_ident)},
148        };
149
150        let output = if is_option {
151            quote! {
152                let #field_ident = &self.#field_ident;
153                if let Some(#field_ident) = #field_ident {
154                    query.push_str(#vec_fn);
155                }
156            }
157        } else {
158            quote! {
159                let #field_ident = &self.#field_ident;
160                query.push_str(#vec_fn);
161            }
162        };
163        tokens.push(output);
164    }
165    tokens
166}
167
168fn is_comma_or_indexed(field: &Field) -> VecMethod {
169    let is_vec_marked = &field
170        .attrs
171        .iter()
172        .filter_map(|attribute| attribute.parse_meta().ok())
173        .map(|meta| match meta {
174            Meta::Path(path) => path,
175            _ => unimplemented!(),
176        })
177        .map(|path| path.get_ident().unwrap().to_string())
178        .collect::<String>();
179
180    match is_vec_marked {
181        f if f == META_INDEXED => VecMethod::Indexed,
182        f if f == META_COMMA => VecMethod::Comma,
183        _ => VecMethod::None,
184    }
185}
186
187const fn get_fields(derive_input: &syn::DeriveInput) -> Option<&SimpleFields> {
188    if let Data::Struct(data_struct) = &derive_input.data {
189        if let Fields::Named(fields) = &data_struct.fields {
190            Some(&fields.named)
191        } else {
192            None
193        }
194    } else {
195        None
196    }
197}
198
199fn is_option(kind: &syn::Type) -> bool {
200    match kind {
201        syn::Type::Path(t) => match t.path.segments.first() {
202            Some(t) => t.ident == "Option",
203            _ => false,
204        },
205        _ => false,
206    }
207}