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}