A Rust procedural macro that generates HTTP client providers with compile-time endpoint definitions. This macro eliminates boilerplate code for creating HTTP clients by automatically generating methods for your API endpoints.
- π Zero runtime overhead - All HTTP client code is generated at compile time
- π§ Automatic method generation - Function names auto-generated from HTTP method and path
- π― Type-safe requests/responses - Full Rust type checking for all parameters
- π Full HTTP method support - GET, POST, PUT, DELETE
- π Path parameters - Dynamic URL path substitution with
{param}
syntax - π Query parameters - Automatic query string serialization
- π Custom headers - Per-request header support
- β‘ Async/await - Built on reqwest with full async support
- β±οΈ Configurable timeouts - Per-client timeout configuration
Add this to your Cargo.toml
:
[dependencies]
http-provider-macro = "0.1.0"
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
use http_provider_macro::http_provider;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct User {
id: u32,
name: String,
email: String,
}
#[derive(Serialize)]
struct CreateUserRequest {
name: String,
email: String,
}
// Define your HTTP provider
http_provider!(
UserApiProvider,
{
{
path: "/users",
method: GET,
res: Vec<User>,
},
{
path: "/users",
method: POST,
req: CreateUserRequest,
res: User,
},
{
path: "/users/{id}",
method: GET,
path_params: PathParams,
res: User,
}
}
);
#[derive(Serialize)]
struct PathParams {
id: u32,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let base_url = reqwest::Url::parse("https://api.example.com")?;
let client = UserApiProvider::new(base_url, 30); // 30 second timeout
// GET /users - auto-generated method name: get_users
let users = client.get_users().await?;
println!("Users: {:?}", users);
// POST /users - auto-generated method name: post_users
let new_user = client.post_users(&CreateUserRequest {
name: "John Doe".to_string(),
email: "john@example.com".to_string(),
}).await?;
println!("Created user: {:?}", new_user);
// GET /users/{id} - auto-generated method name: get_users_id
let user = client.get_users_id(&PathParams { id: 1 }).await?;
println!("User: {:?}", user);
Ok(())
}
Each endpoint is defined within braces {}
with the following fields:
path
: The API endpoint path (string literal)method
: HTTP method (GET
,POST
,PUT
,DELETE
)res
: Response type that implementsDeserialize
fn_name
: Custom function name (defaults to auto-generated)req
: Request body type that implementsSerialize
headers
: Header type (typicallyreqwest::header::HeaderMap
)query_params
: Query parameters type that implementsSerialize
path_params
: Path parameters type with fields matching{param}
in path
use reqwest::header::HeaderMap;
http_provider!(
ApiProvider,
{
{
path: "/protected/data",
method: GET,
fn_name: fetch_protected_data,
res: ApiResponse,
headers: HeaderMap,
}
}
);
// Usage
let mut headers = HeaderMap::new();
headers.insert("Authorization", "Bearer token123".parse()?);
let data = client.fetch_protected_data(headers).await?;
#[derive(Serialize)]
struct SearchQuery {
q: String,
limit: u32,
offset: u32,
}
http_provider!(
SearchProvider,
{
{
path: "/search",
method: GET,
query_params: SearchQuery,
res: SearchResults,
}
}
);
// Usage
let results = client.get_search(&SearchQuery {
q: "rust".to_string(),
limit: 10,
offset: 0,
}).await?;
#[derive(Serialize)]
struct ResourcePath {
user_id: u32,
resource_id: String,
}
http_provider!(
ResourceProvider,
{
{
path: "/users/{user_id}/resources/{resource_id}",
method: GET,
path_params: ResourcePath,
res: Resource,
}
}
);
// Usage
let resource = client.get_users_user_id_resources_resource_id(&ResourcePath {
user_id: 123,
resource_id: "abc-def".to_string(),
}).await?;
http_provider!(
CompleteProvider,
{
{
path: "/api/v1/users/{user_id}/posts",
method: POST,
fn_name: create_user_post,
path_params: UserPath,
req: CreatePostRequest,
res: Post,
headers: HeaderMap,
query_params: PostQuery,
}
}
);
// Usage
let post = client.create_user_post(
&UserPath { user_id: 123 },
&CreatePostRequest { title: "Hello".to_string() },
headers,
&PostQuery { draft: false },
).await?;
The macro generates:
- Struct Definition: A provider struct with
url
,client
, andtimeout
fields - Constructor:
new(url: reqwest::Url, timeout: u64) -> Self
- HTTP Methods: One async method per endpoint definition
Generated methods follow this pattern:
pub async fn method_name(
&self,
path_params: &PathParamsType, // if path_params specified
body: &RequestType, // if req specified
headers: HeaderMap, // if headers specified
query: &QueryType, // if query_params specified
) -> Result<ResponseType, String>
When fn_name
is not specified, names are generated as:
{method}_{path}
where path slashes become underscores- Examples:
GET /users
βget_users
POST /api/v1/posts
βpost_api_v1_posts
PUT /users/{id}
βput_users_id
All generated methods return Result<T, String>
where errors include:
- URL construction errors: Invalid path parameter substitution
- Network errors: Connection timeouts, DNS failures, etc.
- HTTP errors: Non-2xx status codes with status information
- Deserialization errors: JSON parsing failures
- Rust 1.70+: For latest async/await and procedural macro features
- reqwest: HTTP client library
- serde: Serialization framework
- tokio: Async runtime
Licensed under either of
- Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contributions are welcome! Please feel free to submit a Pull Request.