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
11const 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 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 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 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 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}