pincer_macro/lib.rs
1//! Procedural macros for pincer declarative HTTP client.
2//!
3//! This crate provides the proc-macros for declaring HTTP clients:
4//! - `#[pincer]` - Mark a trait as a pincer HTTP client
5//! - `#[get]`, `#[post]`, `#[put]`, `#[delete]`, `#[patch]`, `#[head]`, `#[options]` - HTTP method attributes
6//! - `#[http("VERB /path")]` - Custom HTTP method attribute for extensibility
7//! - `#[path]`, `#[query]`, `#[header]`, `#[body]`, `#[form]` - Parameter attributes
8//! - `#[derive(Query)]` - Derive macro for struct-based query parameters
9//!
10//! # Example
11//!
12//! ```ignore
13//! use pincer::prelude::*;
14//!
15//! #[pincer(url = "https://api.github.com")]
16//! pub trait GitHubApi {
17//! #[get("/users/{username}")]
18//! async fn get_user(&self, #[path] username: &str) -> pincer::Result<User>;
19//! }
20//!
21//! // Usage:
22//! let client = GitHubApiClient::builder().build();
23//! let user = client.get_user("octocat").await?;
24//! ```
25
26mod attrs;
27mod codegen;
28mod expand;
29mod query_derive;
30
31use proc_macro::TokenStream;
32
33use crate::attrs::HttpMethod;
34
35/// Mark a trait as a pincer HTTP client.
36///
37/// This macro generates:
38/// - A clean trait (without pincer attributes)
39/// - A client struct implementing the trait (e.g., `GitHubApiClient`)
40/// - A builder struct for constructing the client (e.g., `GitHubApiClientBuilder`)
41///
42/// # Attributes
43///
44/// - `url` (required): The base URL for the client
45/// - `user_agent` (optional): Custom User-Agent header
46///
47/// # Example
48///
49/// ```ignore
50/// #[pincer(url = "https://api.github.com")]
51/// pub trait GitHubApi {
52/// #[get("/users/{username}")]
53/// async fn get_user(&self, #[path] username: &str) -> pincer::Result<User>;
54/// }
55///
56/// // Usage:
57/// let client = GitHubApiClientBuilder::default().build();
58/// let user = client.get_user("octocat").await?;
59/// ```
60#[proc_macro_attribute]
61pub fn pincer(attr: TokenStream, item: TokenStream) -> TokenStream {
62 expand::expand_pincer_trait(attr.into(), item.into())
63 .unwrap_or_else(|e| e.to_compile_error())
64 .into()
65}
66
67/// Mark a method as a GET request.
68///
69/// # Example
70///
71/// ```ignore
72/// #[get("/users/{id}")]
73/// pub async fn get_user(&self, #[path] id: u64) -> pincer::Result<User>;
74/// ```
75#[proc_macro_attribute]
76pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
77 expand::expand_http_method(HttpMethod::Get, attr.into(), item.into())
78 .unwrap_or_else(|e| e.to_compile_error())
79 .into()
80}
81
82/// Mark a method as a POST request.
83///
84/// # Example
85///
86/// ```ignore
87/// #[post("/users")]
88/// pub async fn create_user(&self, #[body] user: &CreateUser) -> pincer::Result<User>;
89/// ```
90#[proc_macro_attribute]
91pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
92 expand::expand_http_method(HttpMethod::Post, attr.into(), item.into())
93 .unwrap_or_else(|e| e.to_compile_error())
94 .into()
95}
96
97/// Mark a method as a PUT request.
98///
99/// # Example
100///
101/// ```ignore
102/// #[put("/users/{id}")]
103/// pub async fn update_user(&self, #[path] id: u64, #[body] user: &UpdateUser) -> pincer::Result<User>;
104/// ```
105#[proc_macro_attribute]
106pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
107 expand::expand_http_method(HttpMethod::Put, attr.into(), item.into())
108 .unwrap_or_else(|e| e.to_compile_error())
109 .into()
110}
111
112/// Mark a method as a DELETE request.
113///
114/// # Example
115///
116/// ```ignore
117/// #[delete("/users/{id}")]
118/// pub async fn delete_user(&self, #[path] id: u64) -> pincer::Result<()>;
119/// ```
120#[proc_macro_attribute]
121pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
122 expand::expand_http_method(HttpMethod::Delete, attr.into(), item.into())
123 .unwrap_or_else(|e| e.to_compile_error())
124 .into()
125}
126
127/// Mark a method as a PATCH request.
128///
129/// # Example
130///
131/// ```ignore
132/// #[patch("/users/{id}")]
133/// pub async fn patch_user(&self, #[path] id: u64, #[body] patch: &PatchUser) -> pincer::Result<User>;
134/// ```
135#[proc_macro_attribute]
136pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
137 expand::expand_http_method(HttpMethod::Patch, attr.into(), item.into())
138 .unwrap_or_else(|e| e.to_compile_error())
139 .into()
140}
141
142/// Mark a method as a HEAD request.
143///
144/// # Example
145///
146/// ```ignore
147/// #[head("/users/{id}")]
148/// pub async fn check_user(&self, #[path] id: u64) -> pincer::Result<()>;
149/// ```
150#[proc_macro_attribute]
151pub fn head(attr: TokenStream, item: TokenStream) -> TokenStream {
152 expand::expand_http_method(HttpMethod::Head, attr.into(), item.into())
153 .unwrap_or_else(|e| e.to_compile_error())
154 .into()
155}
156
157/// Mark a method as an OPTIONS request.
158///
159/// # Example
160///
161/// ```ignore
162/// #[options("/users")]
163/// pub async fn user_options(&self) -> pincer::Result<()>;
164/// ```
165#[proc_macro_attribute]
166pub fn options(attr: TokenStream, item: TokenStream) -> TokenStream {
167 expand::expand_http_method(HttpMethod::Options, attr.into(), item.into())
168 .unwrap_or_else(|e| e.to_compile_error())
169 .into()
170}
171
172/// Mark a method with a custom HTTP method and path.
173///
174/// This is useful for less common HTTP methods or when you want to
175/// specify the method and path in a single attribute.
176///
177/// # Example
178///
179/// ```ignore
180/// #[http("GET /users/{id}")]
181/// pub async fn get_user(&self, #[path] id: u64) -> pincer::Result<User>;
182///
183/// #[http("OPTIONS /users")]
184/// pub async fn user_options(&self) -> pincer::Result<()>;
185/// ```
186#[proc_macro_attribute]
187pub fn http(attr: TokenStream, item: TokenStream) -> TokenStream {
188 expand::expand_custom_http(attr.into(), item.into())
189 .unwrap_or_else(|e| e.to_compile_error())
190 .into()
191}
192
193/// Derive the `ToQueryPairs` trait for a struct.
194///
195/// This generates a method to convert the struct into query parameter pairs.
196///
197/// # Struct Attributes
198///
199/// - `#[query(rename_all = "camelCase")]` - Rename all fields using a case convention
200///
201/// Supported case conventions:
202/// - `lowercase`, `UPPERCASE`
203/// - `camelCase`, `PascalCase`
204/// - `snake_case`, `SCREAMING_SNAKE_CASE`
205/// - `kebab-case`, `SCREAMING-KEBAB-CASE`
206///
207/// # Field Attributes
208///
209/// - `#[query(skip_none)]` - Skip the field if it's `None` (default for `Option<T>`)
210/// - `#[query(rename = "name")]` - Use a different name in the query string (overrides `rename_all`)
211/// - `#[query(format = "csv")]` - Collection format for `Vec<T>` (csv, ssv, pipes, multi)
212///
213/// # Example
214///
215/// ```ignore
216/// use pincer::Query;
217///
218/// #[derive(Query)]
219/// #[query(rename_all = "camelCase")]
220/// struct SearchParams {
221/// search_query: String, // becomes "searchQuery"
222/// page_number: Option<u32>, // becomes "pageNumber"
223/// #[query(rename = "limit")] // explicit rename overrides rename_all
224/// per_page: u32,
225/// #[query(format = "csv")]
226/// tag_list: Vec<String>, // becomes "tagList"
227/// }
228/// ```
229#[proc_macro_derive(Query, attributes(query))]
230pub fn derive_query(input: TokenStream) -> TokenStream {
231 query_derive::expand_query_derive(input.into())
232 .unwrap_or_else(|e| e.to_compile_error())
233 .into()
234}