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}