Skip to main content

polyoxide_core/
macros.rs

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