http_provider_macro/
lib.rs

1//! # HTTP Provider Macro
2//!
3//! A procedural macro for generating HTTP client providers with compile-time endpoint definitions.
4//! This macro eliminates boilerplate code when creating HTTP clients by automatically generating
5//! methods for your API endpoints.
6//!
7//! ## Features
8//!
9//! - **Zero runtime overhead** - All HTTP client code is generated at compile time
10//! - **Automatic method generation** - Function names auto-generated from HTTP method and path
11//! - **Type-safe requests/responses** - Full Rust type checking for all parameters
12//! - **Full HTTP method support** - GET, POST, PUT, DELETE
13//! - **Path parameters** - Dynamic URL path substitution with `{param}` syntax
14//! - **Query parameters** - Automatic query string serialization
15//! - **Custom headers** - Per-request header support
16//! - **Async/await** - Built on reqwest with full async support
17//! - **Configurable timeouts** - Per-client timeout configuration
18//!
19//! ## Quick Start
20//!
21//! ```rust
22//! use http_provider_macro::http_provider;
23//! use serde::{Deserialize, Serialize};
24//!
25//! #[derive(Serialize, Deserialize, Debug)]
26//! struct User {
27//!     id: u32,
28//!     name: String,
29//! }
30//!
31//! #[derive(Serialize)]
32//! struct CreateUser {
33//!     name: String,
34//! }
35//!
36//! // Define your HTTP provider
37//! http_provider!(
38//!     UserApi,
39//!     {
40//!         {
41//!             path: "/users",
42//!             method: GET,
43//!             res: Vec<User>,
44//!         },
45//!         {
46//!             path: "/users",
47//!             method: POST,
48//!             req: CreateUser,
49//!             res: User,
50//!         }
51//!     }
52//! );
53//!
54//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
55//! let base_url = reqwest::Url::parse("https://api.example.com")?;
56//! let client = UserApi::new(base_url, 30);
57//!
58//! // Auto-generated methods
59//! let users = client.get_users().await?;
60//! let new_user = client.post_users(&CreateUser {
61//!     name: "John".to_string()
62//! }).await?;
63//! # Ok(())
64//! # }
65//! ```
66//!
67//! ## Endpoint Configuration
68//!
69//! Each endpoint is defined within braces with these fields:
70//!
71//! ### Required Fields
72//! - `path`: API endpoint path (string literal)
73//! - `method`: HTTP method (GET, POST, PUT, DELETE)
74//! - `res`: Response type implementing `serde::Deserialize`
75//!
76//! ### Optional Fields
77//! - `fn_name`: Custom function name (auto-generated if omitted)
78//! - `req`: Request body type implementing `serde::Serialize`
79//! - `headers`: Header type (typically `reqwest::header::HeaderMap`)
80//! - `query_params`: Query parameters type implementing `serde::Serialize`
81//! - `path_params`: Path parameters type with fields matching `{param}` in path
82//!
83//! ## Examples
84//!
85//! ### Path Parameters
86//!
87//! ```rust
88//! # use http_provider_macro::http_provider;
89//! # use serde::{Deserialize, Serialize};
90//! #[derive(Serialize)]
91//! struct UserPath {
92//!     id: u32,
93//! }
94//!
95//! #[derive(Deserialize)]
96//! struct User {
97//!     id: u32,
98//!     name: String,
99//! }
100//!
101//! http_provider!(
102//!     UserApi,
103//!     {
104//!         {
105//!             path: "/users/{id}",
106//!             method: GET,
107//!             path_params: UserPath,
108//!             res: User,
109//!         }
110//!     }
111//! );
112//! ```
113//!
114//! ### Query Parameters and Headers
115//!
116//! ```rust
117//! # use http_provider_macro::http_provider;
118//! # use serde::{Deserialize, Serialize};
119//! # use reqwest::header::HeaderMap;
120//! #[derive(Serialize)]
121//! struct SearchQuery {
122//!     q: String,
123//!     limit: u32,
124//! }
125//!
126//! #[derive(Deserialize)]
127//! struct SearchResults {
128//!     results: Vec<String>,
129//! }
130//!
131//! http_provider!(
132//!     SearchApi,
133//!     {
134//!         {
135//!             path: "/search",
136//!             method: GET,
137//!             fn_name: search_items,
138//!             query_params: SearchQuery,
139//!             headers: HeaderMap,
140//!             res: SearchResults,
141//!         }
142//!     }
143//! );
144//! ```
145
146extern crate proc_macro;
147
148use crate::{
149    error::{MacroError, MacroResult},
150    input::{EndpointDef, HttpMethod, HttpProviderInput},
151};
152use heck::ToSnakeCase;
153use quote::quote;
154use regex::Regex;
155use syn::{parse_macro_input, Ident};
156
157mod error;
158mod input;
159
160/// Generates an HTTP client provider struct with methods for each defined endpoint.
161///
162/// This macro takes a struct name and a list of endpoint definitions, generating
163/// a complete HTTP client with methods for each endpoint.
164///
165/// # Syntax
166///
167/// ```text
168/// http_provider!(
169///     StructName,
170///     {
171///         {
172///             path: "/endpoint/path",
173///             method: HTTP_METHOD,
174///             [fn_name: custom_function_name,]
175///             [req: RequestType,]
176///             res: ResponseType,
177///             [headers: HeaderType,]
178///             [query_params: QueryType,]
179///             [path_params: PathParamsType,]
180///         },
181///         // ... more endpoints
182///     }
183/// );
184/// ```
185///
186/// # Generated Structure
187///
188/// The macro generates:
189/// - A struct with `url`, `client`, and `timeout` fields
190/// - A `new(url: reqwest::Url, timeout: u64)` constructor
191/// - One async method per endpoint definition
192///
193/// # Method Naming
194///
195/// When `fn_name` is not provided, method names are auto-generated as:
196/// `{method}_{path}` where path separators become underscores.
197///
198/// # Examples
199///
200/// ```rust
201/// use http_provider_macro::http_provider;
202/// use serde::{Deserialize, Serialize};
203///
204/// #[derive(Serialize, Deserialize)]
205/// struct User {
206///     id: u32,
207///     name: String,
208/// }
209///
210/// http_provider!(
211///     UserClient,
212///     {
213///         {
214///             path: "/users",
215///             method: GET,
216///             res: Vec<User>,
217///         },
218///         {
219///             path: "/users/{id}",
220///             method: GET,
221///             path_params: UserPath,
222///             res: User,
223///         }
224///     }
225/// );
226///
227/// #[derive(Serialize)]
228/// struct UserPath {
229///     id: u32,
230/// }
231/// ```
232#[proc_macro]
233pub fn http_provider(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
234    let parsed = parse_macro_input!(input as HttpProviderInput);
235
236    let mut expander = HttpProviderMacroExpander::new();
237
238    match expander.expand(parsed) {
239        Ok(tokens) => tokens.into(),
240        Err(err) => err.to_compile_error().into(),
241    }
242}
243
244/// Main expander that generates the HTTP provider struct and its methods.
245struct HttpProviderMacroExpander;
246
247impl HttpProviderMacroExpander {
248    fn new() -> Self {
249        Self
250    }
251
252    /// Expands the macro input into a complete HTTP provider implementation.
253    fn expand(&mut self, input: HttpProviderInput) -> MacroResult<proc_macro2::TokenStream> {
254        if input.endpoints.is_empty() {
255            return Err(MacroError::Custom {
256                message: "No endpoints defined".to_string(),
257                span: input.struct_name.span(),
258            });
259        }
260
261        let struct_name = input.struct_name;
262
263        let methods: Vec<proc_macro2::TokenStream> = input
264            .endpoints
265            .iter()
266            .map(|endpoint| self.expand_method(endpoint))
267            .collect::<Result<_, _>>()?;
268
269        Ok(quote! {
270            pub struct #struct_name {
271                url: reqwest::Url,
272                client: reqwest::Client,
273                timeout: std::time::Duration,
274            }
275
276            impl #struct_name {
277                /// Creates a new HTTP provider instance.
278                ///
279                /// # Arguments
280                /// * `url` - Base URL for all requests
281                /// * `timeout` - Request timeout in milliseconds
282                pub fn new(url: reqwest::Url, timeout: u64) -> Self {
283                    let client = reqwest::Client::new();
284                    let timeout = std::time::Duration::from_millis(timeout);
285                    Self { url, client, timeout }
286                }
287
288                #(#methods)*
289            }
290        })
291    }
292
293    /// Generates a single HTTP method for an endpoint definition.
294    fn expand_method(&self, endpoint: &EndpointDef) -> MacroResult<proc_macro2::TokenStream> {
295        let method_expander = MethodExpander::new(endpoint);
296
297        let fn_signature = method_expander.expand_fn_signature();
298        let url_construction = method_expander.build_url_construction();
299        let request_building = method_expander.build_request();
300        let response_handling = method_expander.build_response_handling();
301
302        Ok(quote! {
303            #fn_signature {
304                #url_construction
305                #request_building
306                #response_handling
307            }
308        })
309    }
310}
311/// Handles the expansion of individual HTTP method implementations
312struct MethodExpander<'a> {
313    def: &'a EndpointDef,
314}
315
316impl<'a> MethodExpander<'a> {
317    fn new(def: &'a EndpointDef) -> Self {
318        Self { def }
319    }
320
321    /// Generates the function signature for an endpoint method.
322    fn expand_fn_signature(&self) -> proc_macro2::TokenStream {
323        let path = self.def.path.value();
324        let method = &self.def.method;
325
326        let fn_name = if let Some(ref name) = self.def.fn_name {
327            name.clone()
328        } else {
329            let method_str = format!("{:?}", method).to_lowercase();
330            let path_str = path.trim_start_matches('/').replace("/", "_");
331            let auto_name = format!("{}_{}", method_str, path_str).to_snake_case();
332            Ident::new(&auto_name, self.def.path.span())
333        };
334
335        let res = &self.def.res;
336
337        let mut params = vec![];
338
339        if let Some(path_params) = &self.def.path_params {
340            params.push(quote! { path_params: &#path_params });
341        }
342        if let Some(body) = &self.def.req {
343            params.push(quote! { body: &#body });
344        }
345        if let Some(headers) = &self.def.headers {
346            params.push(quote! { headers: #headers });
347        }
348        if let Some(query_params) = &self.def.query_params {
349            params.push(quote! { query_params: &#query_params });
350        }
351
352        quote! {
353            pub async fn #fn_name(&self, #(#params),*) -> Result<#res, String>
354        }
355    }
356
357    /// Generates URL construction logic, handling path parameter substitution.
358    fn build_url_construction(&self) -> proc_macro2::TokenStream {
359        let path = self.def.path.value();
360
361        if self.def.path_params.is_some() {
362            let re = Regex::new(r"\{([a-zA-Z0-9_]+)\}").unwrap();
363            let mut replacements = Vec::new();
364
365            for cap in re.captures_iter(&path) {
366                let param_name = &cap[1];
367                let ident = Ident::new(param_name, proc_macro2::Span::call_site());
368                replacements.push(quote! {
369                    path = path.replace(concat!("{", #param_name, "}"), &path_params.#ident.to_string());
370                });
371            }
372
373            quote! {
374                let mut path = #path.to_string();
375                #(#replacements)*
376                let url = self.url.join(&path)
377                    .map_err(|e| format!("Failed to construct URL: {}", e))?;
378            }
379        } else {
380            quote! {
381                let url = self.url.join(#path)
382                    .map_err(|e| format!("Failed to construct URL: {}", e))?;
383            }
384        }
385    }
386
387    /// Generates request building logic including body, headers, and query parameters
388    fn build_request(&self) -> proc_macro2::TokenStream {
389        let method_call = match self.def.method {
390            HttpMethod::GET => quote! { self.client.get(url) },
391            HttpMethod::POST => quote! { self.client.post(url) },
392            HttpMethod::PUT => quote! { self.client.put(url) },
393            HttpMethod::DELETE => quote! { self.client.delete(url) },
394        };
395
396        let mut request_modifications = Vec::new();
397
398        // Add body handling
399        if self.def.req.is_some() {
400            request_modifications.push(quote! {
401                request = request.json(body);
402            });
403        }
404
405        if self.def.query_params.is_some() {
406            request_modifications.push(quote! {
407                request = request.query(query_params);
408            });
409        }
410
411        // Add headers
412        if self.def.headers.is_some() {
413            request_modifications.push(quote! {
414                let request = request.headers(headers);
415            });
416        }
417
418        quote! {
419            let mut request = #method_call;
420            #(#request_modifications)*
421        }
422    }
423
424    /// Generates response handling logic.
425    fn build_response_handling(&self) -> proc_macro2::TokenStream {
426        let res = &self.def.res;
427
428        quote! {
429            let response = request
430                .send()
431                .await
432                .map_err(|e| format!("Request failed: {}", e))?;
433
434            let status = response.status();
435            if !status.is_success() {
436                return Err(format!("HTTP request failed with status {}: {}",
437                    status.as_u16(),
438                    status.canonical_reason().unwrap_or("Unknown error")
439                ).into());
440            }
441
442            let result: #res = response
443                .json()
444                .await
445                .map_err(|e| format!("Failed to deserialize response: {}", e))?;
446
447            Ok(result)
448        }
449    }
450}