Skip to main content

polyoxide_core/
macros.rs

1//! Macros for reducing boilerplate in API client implementations
2
3/// Generates a client builder's five transport setters over a
4/// `ClientConfig` field: `base_url`, `timeout_ms`, `pool_size`,
5/// `with_retry_config` and `max_concurrent`.
6///
7/// Invoke it inside the builder's `impl`, naming the field that holds the
8/// config. The builder's `build` reads the config back through
9/// `ClientConfig::http_builder`.
10///
11/// ```
12/// use polyoxide_core::ClientConfig;
13///
14/// pub struct MyBuilder {
15///     config: ClientConfig,
16/// }
17///
18/// impl MyBuilder {
19///     polyoxide_core::client_config_setters!(config);
20/// }
21///
22/// let builder = MyBuilder { config: ClientConfig::new("https://example.com", 4) }
23///     .timeout_ms(5_000)
24///     .max_concurrent(2);
25/// assert_eq!(builder.config.timeout_ms, 5_000);
26/// assert_eq!(builder.config.max_concurrent, Some(2));
27/// ```
28#[macro_export]
29macro_rules! client_config_setters {
30    ($config:ident) => {
31        /// Set the base URL for the API.
32        pub fn base_url(mut self, url: impl Into<String>) -> Self {
33            self.$config.base_url = url.into();
34            self
35        }
36
37        /// Set the request timeout in milliseconds (default 30,000).
38        pub fn timeout_ms(mut self, timeout: u64) -> Self {
39            self.$config.timeout_ms = timeout;
40            self
41        }
42
43        /// Set the number of idle connections kept per host (default 10).
44        pub fn pool_size(mut self, size: usize) -> Self {
45            self.$config.pool_size = size;
46            self
47        }
48
49        /// Set the retry schedule: how many retries, and the backoff between
50        /// them.
51        pub fn with_retry_config(mut self, config: $crate::RetryConfig) -> Self {
52            self.$config.retry_config = Some(config);
53            self
54        }
55
56        /// Set the maximum number of concurrent in-flight requests.
57        ///
58        /// Each client has its own default. At least 1: zero admits no
59        /// request, so every send waits forever.
60        pub fn max_concurrent(mut self, max: usize) -> Self {
61            self.$config.max_concurrent = Some(max);
62            self
63        }
64    };
65}
66
67/// Generates a client's namespace accessors: each clones the listed client
68/// fields into the same-named fields of its namespace struct.
69///
70/// The invocation starts with the fields every namespace takes, then lists
71/// one accessor per namespace, with its attributes. An accessor may instead
72/// name its own field mapping, `name: Type { to: from, .. }`, which clones
73/// `self.from` into `Type::to`. The namespace structs stay declared by hand,
74/// since their paths are public API.
75///
76/// ```
77/// #[derive(Clone)]
78/// pub struct Http;
79///
80/// pub struct Markets {
81///     http_client: Http,
82/// }
83///
84/// pub struct Pnl {
85///     http_client: Http,
86/// }
87///
88/// pub struct Client {
89///     http_client: Http,
90///     pnl_http_client: Http,
91/// }
92///
93/// impl Client {
94///     polyoxide_core::namespaces! { http_client;
95///         /// The markets namespace.
96///         markets: Markets,
97///         /// The PnL namespace, on its own host.
98///         pnl: Pnl { http_client: pnl_http_client },
99///     }
100/// }
101/// ```
102#[macro_export]
103macro_rules! namespaces {
104    (@accessors [$($field:ident),+]) => {};
105    (@accessors [$($field:ident),+]
106        $(#[$meta:meta])* $name:ident: $ns:path { $($to:ident: $from:ident),+ $(,)? }
107        $(, $($rest:tt)*)?
108    ) => {
109        $(#[$meta])*
110        pub fn $name(&self) -> $ns {
111            $ns { $($to: self.$from.clone()),+ }
112        }
113
114        $crate::namespaces!(@accessors [$($field),+] $($($rest)*)?);
115    };
116    (@accessors [$($field:ident),+]
117        $(#[$meta:meta])* $name:ident: $ns:path
118        $(, $($rest:tt)*)?
119    ) => {
120        $(#[$meta])*
121        pub fn $name(&self) -> $ns {
122            $ns { $($field: self.$field.clone()),+ }
123        }
124
125        $crate::namespaces!(@accessors [$($field),+] $($($rest)*)?);
126    };
127    ($($field:ident),+; $($accessors:tt)*) => {
128        $crate::namespaces!(@accessors [$($field),+] $($accessors)*);
129    };
130}
131
132/// Generates a request builder's query-parameter setters.
133///
134/// Each setter writes its parameter to `self.request`, or to
135/// `self.<field>` when the invocation starts with `self.<field>;`. The field
136/// may be core's `Request`, or any type with a `query(key, impl ToString)`
137/// method of its own, which is then preferred (and `query_many`, for the
138/// `many` arm). Each setter keeps the attributes written above it, its doc
139/// comment included.
140///
141/// The arms, one per setter, separated by commas:
142///
143/// - `name: T => "key"` takes a `T` and sends it as `key`.
144/// - `name: impl Into<String> => "key"` takes anything that converts into a
145///   `String`.
146/// - `name: many T => "key"` takes `T`, an iterator, and repeats `key` once
147///   per value.
148/// - `name: csv T => "key"` takes `T`, an iterator, and sends its values
149///   comma-joined, or nothing when the joined value is empty.
150/// - `name: csv<I, S> => "key"` is the same, with explicit generics
151///   (`I: IntoIterator<Item = S>`, `S: ToString`).
152/// - `name(arg: T) => "key" = expr` sends `expr`, computed from `arg`.
153/// - `name(arg: T) => "key" if cond` sends `arg` only when `cond` holds.
154/// - `name(arg: T) => csv "key" = expr` comma-joins the iterator `expr`.
155///
156/// ```
157/// use polyoxide_core::{ApiError, HttpClientBuilder, Request};
158///
159/// pub struct ListThings {
160///     request: Request<Vec<String>, ApiError>,
161/// }
162///
163/// impl ListThings {
164///     polyoxide_core::query_setters! {
165///         /// Page size.
166///         limit: u32 => "limit",
167///         /// Only things with these ids.
168///         ids: many impl IntoIterator<Item = i64> => "id",
169///         /// Only open things.
170///         open(open: bool) => "closed" = !open,
171///     }
172/// }
173///
174/// let http = HttpClientBuilder::new("https://example.com").build().unwrap();
175/// let _things = ListThings { request: Request::new(http, "/things") }
176///     .limit(10)
177///     .ids([1, 2])
178///     .open(true);
179/// ```
180#[macro_export]
181macro_rules! query_setters {
182    (@setters $field:ident;) => {};
183    (@setters $field:ident;
184        $(#[$meta:meta])* $name:ident: impl Into<String> => $key:literal
185        $(, $($rest:tt)*)?
186    ) => {
187        $(#[$meta])*
188        pub fn $name(mut self, value: impl Into<String>) -> Self {
189            #[allow(unused_imports)]
190            use $crate::QueryBuilder as _;
191            self.$field = self.$field.query($key, value.into());
192            self
193        }
194
195        $crate::query_setters!(@setters $field; $($($rest)*)?);
196    };
197    (@setters $field:ident;
198        $(#[$meta:meta])* $name:ident: many $ty:ty => $key:literal
199        $(, $($rest:tt)*)?
200    ) => {
201        $(#[$meta])*
202        pub fn $name(mut self, values: $ty) -> Self {
203            #[allow(unused_imports)]
204            use $crate::QueryBuilder as _;
205            self.$field = self.$field.query_many($key, values);
206            self
207        }
208
209        $crate::query_setters!(@setters $field; $($($rest)*)?);
210    };
211    (@setters $field:ident;
212        $(#[$meta:meta])* $name:ident: csv<I, S> => $key:literal
213        $(, $($rest:tt)*)?
214    ) => {
215        $(#[$meta])*
216        pub fn $name<I, S>(mut self, values: I) -> Self
217        where
218            I: IntoIterator<Item = S>,
219            S: ToString,
220        {
221            #[allow(unused_imports)]
222            use $crate::QueryBuilder as _;
223            if let Some(joined) = $crate::csv(values) {
224                self.$field = self.$field.query($key, joined);
225            }
226            self
227        }
228
229        $crate::query_setters!(@setters $field; $($($rest)*)?);
230    };
231    (@setters $field:ident;
232        $(#[$meta:meta])* $name:ident: csv $ty:ty => $key:literal
233        $(, $($rest:tt)*)?
234    ) => {
235        $(#[$meta])*
236        pub fn $name(mut self, values: $ty) -> Self {
237            #[allow(unused_imports)]
238            use $crate::QueryBuilder as _;
239            if let Some(joined) = $crate::csv(values) {
240                self.$field = self.$field.query($key, joined);
241            }
242            self
243        }
244
245        $crate::query_setters!(@setters $field; $($($rest)*)?);
246    };
247    (@setters $field:ident;
248        $(#[$meta:meta])* $name:ident($arg:ident: $ty:ty) => csv $key:literal = $value:expr
249        $(, $($rest:tt)*)?
250    ) => {
251        $(#[$meta])*
252        pub fn $name(mut self, $arg: $ty) -> Self {
253            #[allow(unused_imports)]
254            use $crate::QueryBuilder as _;
255            if let Some(joined) = $crate::csv($value) {
256                self.$field = self.$field.query($key, joined);
257            }
258            self
259        }
260
261        $crate::query_setters!(@setters $field; $($($rest)*)?);
262    };
263    (@setters $field:ident;
264        $(#[$meta:meta])* $name:ident($arg:ident: $ty:ty) => $key:literal = $value:expr
265        $(, $($rest:tt)*)?
266    ) => {
267        $(#[$meta])*
268        pub fn $name(mut self, $arg: $ty) -> Self {
269            #[allow(unused_imports)]
270            use $crate::QueryBuilder as _;
271            self.$field = self.$field.query($key, $value);
272            self
273        }
274
275        $crate::query_setters!(@setters $field; $($($rest)*)?);
276    };
277    (@setters $field:ident;
278        $(#[$meta:meta])* $name:ident($arg:ident: $ty:ty) => $key:literal if $cond:expr
279        $(, $($rest:tt)*)?
280    ) => {
281        $(#[$meta])*
282        pub fn $name(mut self, $arg: $ty) -> Self {
283            #[allow(unused_imports)]
284            use $crate::QueryBuilder as _;
285            if $cond {
286                self.$field = self.$field.query($key, $arg);
287            }
288            self
289        }
290
291        $crate::query_setters!(@setters $field; $($($rest)*)?);
292    };
293    (@setters $field:ident;
294        $(#[$meta:meta])* $name:ident: $ty:ty => $key:literal
295        $(, $($rest:tt)*)?
296    ) => {
297        $(#[$meta])*
298        pub fn $name(mut self, value: $ty) -> Self {
299            #[allow(unused_imports)]
300            use $crate::QueryBuilder as _;
301            self.$field = self.$field.query($key, value);
302            self
303        }
304
305        $crate::query_setters!(@setters $field; $($($rest)*)?);
306    };
307    (self.$field:ident; $($setters:tt)*) => {
308        $crate::query_setters!(@setters $field; $($setters)*);
309    };
310    ($($setters:tt)*) => {
311        $crate::query_setters!(@setters request; $($setters)*);
312    };
313}