acorn/util/macros.rs
1//! Macros
2
3/// Execute a command and capture its output.
4///
5/// Simplifies `Command::new(binary).args(args).output()` patterns.
6/// Thread safe — `Command` is `Send + Sync`.
7///
8/// Supports several calling conventions:
9///
10/// | Prefix | Returns | Description |
11/// |-----------|--------------------------------|-----------------------------------------|
12/// | *(none)* | `io::Result<Output>` | Run and capture output |
13/// | `status` | `io::Result<ExitStatus>` | Exit status only (no output) |
14/// | `sh` | `io::Result<Output>` | Parse string, capture output |
15/// | `sh status` | `io::Result<ExitStatus>` | Parse string, exit status only |
16/// | `bash` | `io::Result<Output>` | Run via `bash -c <command>` |
17/// | `pwsh` | `io::Result<Output>` | Run via `pwsh -NoProfile -Command ...` |
18/// | `try` | `Result<String, String>` | Run, return stdout or error message |
19///
20/// All forms support an optional `; dir: path` suffix to set the working directory.
21///
22/// The `sh` variants use shell-aware word splitting (handles quoted arguments via
23/// the `shell-words` crate).
24///
25/// # Syntax
26///
27/// ```ignore
28/// // String form (shell-aware quoting)
29/// cmd!(sh "git diff --name-only")
30/// cmd!(sh format!("git diff --name-only {branch}"))
31/// cmd!(sh "echo 'hello world'") // handles quotes
32/// cmd!(bash "echo 'hello world'")
33/// cmd!(pwsh "Write-Output 'hello world'")
34///
35/// // CLI-style literals (fastest)
36/// cmd!("git" "diff-tree" "--no-commit-id" "--name-only")
37/// cmd!("git" "log" "--oneline"; dir: repo_path)
38///
39/// // Array literal (dynamic types)
40/// cmd!("git", ["branch", "--show-current"])
41///
42/// // Variable args
43/// cmd!("git", args)
44///
45/// // Try form — returns Result<String, String>
46/// cmd!(try "git" "rev-parse" "HEAD")
47/// cmd!(try "git", args)
48///
49/// // With working directory
50/// cmd!("git" "status"; dir: project_dir)
51/// cmd!(try sh "npm test"; dir: project_dir)
52/// cmd!(status bash "echo hello"; dir: project_dir)
53/// cmd!(try pwsh "Write-Output 'hello'"; dir: project_dir)
54/// ```
55///
56/// # Examples
57///
58/// ```ignore
59/// use acorn::cmd;
60/// use acorn::prelude::CommandOutput;
61///
62/// // String form with shell-aware quoting
63/// let branch = "main";
64/// match cmd!(sh format!("git diff --name-only {branch}")) {
65/// Ok(output) if output.status.success() => {
66/// println!("{}", output.stdout());
67/// }
68/// _ => {},
69/// }
70///
71/// // CLI-style
72/// match cmd!("git" "branch" "--show-current") {
73/// Ok(output) if output.status.success() => {
74/// println!("{}", output.stdout());
75/// }
76/// Ok(output) => eprintln!("{}", output.stderr()),
77/// Err(why) => eprintln!("Error: {}", why),
78/// }
79///
80/// // Try form — simplified error handling
81/// match cmd!(try "git" "rev-parse" "HEAD") {
82/// Ok(hash) => println!("{hash}"),
83/// Err(msg) => eprintln!("failed: {msg}"),
84/// }
85///
86/// // Explicit shell selection
87/// let output = cmd!(bash "echo bash-mode")?;
88/// let output = cmd!(pwsh "Write-Output 'pwsh-mode'")?;
89/// ```
90#[macro_export]
91macro_rules! cmd {
92 // ── sh status ──────────────────────────────────────────────
93 // sh status string + dir
94 (sh status $cmd:expr; dir: $dir:expr) => {{
95 match $crate::util::cmd::parse_sh(&$cmd.to_string()) {
96 Ok((binary, args)) => $crate::util::cmd::run_status(&binary, &args, Some($dir.as_ref())),
97 Err(e) => Err(e),
98 }
99 }};
100 // sh status string (no dir)
101 (sh status $cmd:expr) => {{
102 match $crate::util::cmd::parse_sh(&$cmd.to_string()) {
103 Ok((binary, args)) => $crate::util::cmd::run_status(&binary, &args, None),
104 Err(e) => Err(e),
105 }
106 }};
107 // ── sh output ──────────────────────────────────────────────
108 // sh string + dir
109 (sh $cmd:expr; dir: $dir:expr) => {{
110 match $crate::util::cmd::parse_sh(&$cmd.to_string()) {
111 Ok((binary, args)) => $crate::util::cmd::run_output(&binary, &args, Some($dir.as_ref())),
112 Err(e) => Err(e),
113 }
114 }};
115 // sh string (no dir)
116 (sh $cmd:expr) => {{
117 match $crate::util::cmd::parse_sh(&$cmd.to_string()) {
118 Ok((binary, args)) => $crate::util::cmd::run_output(&binary, &args, None),
119 Err(e) => Err(e),
120 }
121 }};
122 // ── bash status ────────────────────────────────────────────
123 // status bash string + dir
124 (status bash $cmd:expr; dir: $dir:expr) => {{
125 $crate::util::cmd::run_shell_status("bash", &["-c"], &$cmd.to_string(), Some($dir.as_ref()))
126 }};
127 // status bash string (no dir)
128 (status bash $cmd:expr) => {{
129 $crate::util::cmd::run_shell_status("bash", &["-c"], &$cmd.to_string(), None)
130 }};
131 // bash status string + dir
132 (bash status $cmd:expr; dir: $dir:expr) => {{
133 $crate::util::cmd::run_shell_status("bash", &["-c"], &$cmd.to_string(), Some($dir.as_ref()))
134 }};
135 // bash status string (no dir)
136 (bash status $cmd:expr) => {{
137 $crate::util::cmd::run_shell_status("bash", &["-c"], &$cmd.to_string(), None)
138 }};
139 // ── bash output ────────────────────────────────────────────
140 // bash string + dir
141 (bash $cmd:expr; dir: $dir:expr) => {{
142 $crate::util::cmd::run_shell_output("bash", &["-c"], &$cmd.to_string(), Some($dir.as_ref()))
143 }};
144 // bash string (no dir)
145 (bash $cmd:expr) => {{
146 $crate::util::cmd::run_shell_output("bash", &["-c"], &$cmd.to_string(), None)
147 }};
148 // ── pwsh status ────────────────────────────────────────────
149 // status pwsh string + dir
150 (status pwsh $cmd:expr; dir: $dir:expr) => {{
151 $crate::util::cmd::run_shell_status("pwsh", &["-NoProfile", "-Command"], &$cmd.to_string(), Some($dir.as_ref()))
152 }};
153 // status pwsh string (no dir)
154 (status pwsh $cmd:expr) => {{
155 $crate::util::cmd::run_shell_status("pwsh", &["-NoProfile", "-Command"], &$cmd.to_string(), None)
156 }};
157 // pwsh status string + dir
158 (pwsh status $cmd:expr; dir: $dir:expr) => {{
159 $crate::util::cmd::run_shell_status("pwsh", &["-NoProfile", "-Command"], &$cmd.to_string(), Some($dir.as_ref()))
160 }};
161 // pwsh status string (no dir)
162 (pwsh status $cmd:expr) => {{
163 $crate::util::cmd::run_shell_status("pwsh", &["-NoProfile", "-Command"], &$cmd.to_string(), None)
164 }};
165 // ── pwsh output ────────────────────────────────────────────
166 // pwsh string + dir
167 (pwsh $cmd:expr; dir: $dir:expr) => {{
168 $crate::util::cmd::run_shell_output("pwsh", &["-NoProfile", "-Command"], &$cmd.to_string(), Some($dir.as_ref()))
169 }};
170 // pwsh string (no dir)
171 (pwsh $cmd:expr) => {{
172 $crate::util::cmd::run_shell_output("pwsh", &["-NoProfile", "-Command"], &$cmd.to_string(), None)
173 }};
174 // ── try sh ─────────────────────────────────────────────────
175 // try sh string + dir
176 (try sh $cmd:expr; dir: $dir:expr) => {{
177 match $crate::util::cmd::parse_sh(&$cmd.to_string()) {
178 Ok((binary, args)) => $crate::util::cmd::try_from_output(
179 $crate::util::cmd::run_output(&binary, &args, Some($dir.as_ref())),
180 ),
181 Err(e) => Err(format!("{e}")),
182 }
183 }};
184 // try sh string (no dir)
185 (try sh $cmd:expr) => {{
186 match $crate::util::cmd::parse_sh(&$cmd.to_string()) {
187 Ok((binary, args)) => $crate::util::cmd::try_from_output(
188 $crate::util::cmd::run_output(&binary, &args, None),
189 ),
190 Err(e) => Err(format!("{e}")),
191 }
192 }};
193 // ── try bash ───────────────────────────────────────────────
194 // try bash string + dir
195 (try bash $cmd:expr; dir: $dir:expr) => {{
196 $crate::util::cmd::try_from_output(
197 $crate::util::cmd::run_shell_output("bash", &["-c"], &$cmd.to_string(), Some($dir.as_ref())),
198 )
199 }};
200 // try bash string (no dir)
201 (try bash $cmd:expr) => {{
202 $crate::util::cmd::try_from_output(
203 $crate::util::cmd::run_shell_output("bash", &["-c"], &$cmd.to_string(), None),
204 )
205 }};
206 // ── try pwsh ───────────────────────────────────────────────
207 // try pwsh string + dir
208 (try pwsh $cmd:expr; dir: $dir:expr) => {{
209 $crate::util::cmd::try_from_output(
210 $crate::util::cmd::run_shell_output("pwsh", &["-NoProfile", "-Command"], &$cmd.to_string(), Some($dir.as_ref())),
211 )
212 }};
213 // try pwsh string (no dir)
214 (try pwsh $cmd:expr) => {{
215 $crate::util::cmd::try_from_output(
216 $crate::util::cmd::run_shell_output("pwsh", &["-NoProfile", "-Command"], &$cmd.to_string(), None),
217 )
218 }};
219 // ── status CLI-style ───────────────────────────────────────
220 // status CLI-style + dir
221 (status $binary:literal $($arg:literal)*; dir: $dir:expr) => {{
222 $crate::util::cmd::run_status($binary, [$($arg),*], Some($dir.as_ref()))
223 }};
224 // status CLI-style (no dir)
225 (status $binary:literal $($arg:literal)*) => {{
226 $crate::util::cmd::run_status($binary, [$($arg),*], None)
227 }};
228 // ── status array ───────────────────────────────────────────
229 // status array + dir
230 (status $binary:expr, [ $($arg:expr),* $(,)? ]; dir: $dir:expr) => {{
231 let mut __cmd = $crate::prelude::Command::new($binary);
232 __cmd.args([$($arg),*]);
233 __cmd.current_dir($dir);
234 __cmd.status()
235 }};
236 // status array (no dir)
237 (status $binary:expr, [ $($arg:expr),* $(,)? ]) => {{
238 $crate::prelude::Command::new($binary).args([$($arg),*]).status()
239 }};
240 // ── status variable ───────────────────────────────────────
241 // status variable + dir
242 (status $binary:expr, $args:expr; dir: $dir:expr) => {{
243 let mut __cmd = $crate::prelude::Command::new($binary);
244 __cmd.args($args);
245 __cmd.current_dir($dir);
246 __cmd.status()
247 }};
248 // status variable (no dir)
249 (status $binary:expr, $args:expr) => {{
250 $crate::prelude::Command::new($binary).args($args).status()
251 }};
252 // ── try CLI-style ──────────────────────────────────────────
253 // try CLI-style + dir
254 (try $binary:literal $($arg:literal)*; dir: $dir:expr) => {{
255 $crate::util::cmd::try_from_output(
256 $crate::util::cmd::run_output($binary, [$($arg),*], Some($dir.as_ref())),
257 )
258 }};
259 // try CLI-style (no dir)
260 (try $binary:literal $($arg:literal)*) => {{
261 $crate::util::cmd::try_from_output(
262 $crate::util::cmd::run_output($binary, [$($arg),*], None),
263 )
264 }};
265 // ── try array ──────────────────────────────────────────────
266 // try array + dir
267 (try $binary:expr, [ $($arg:expr),* $(,)? ]; dir: $dir:expr) => {{
268 let mut __cmd = $crate::prelude::Command::new($binary);
269 __cmd.args([$($arg),*]);
270 __cmd.current_dir($dir);
271 $crate::util::cmd::try_from_output(__cmd.output())
272 }};
273 // try array (no dir)
274 (try $binary:expr, [ $($arg:expr),* $(,)? ]) => {{
275 $crate::util::cmd::try_from_output(
276 $crate::prelude::Command::new($binary).args([$($arg),*]).output(),
277 )
278 }};
279 // ── try variable ───────────────────────────────────────────
280 // try variable + dir
281 (try $binary:expr, $args:expr; dir: $dir:expr) => {{
282 let mut __cmd = $crate::prelude::Command::new($binary);
283 __cmd.args($args);
284 __cmd.current_dir($dir);
285 $crate::util::cmd::try_from_output(__cmd.output())
286 }};
287 // try variable (no dir)
288 (try $binary:expr, $args:expr) => {{
289 $crate::util::cmd::try_from_output(
290 $crate::prelude::Command::new($binary).args($args).output(),
291 )
292 }};
293 // ── default output CLI-style ──────────────────────────────
294 // CLI-style + dir
295 ($binary:literal $($arg:literal)*; dir: $dir:expr) => {{
296 $crate::util::cmd::run_output($binary, [$($arg),*], Some($dir.as_ref()))
297 }};
298 // CLI-style (no dir)
299 ($binary:literal $($arg:literal)*) => {{
300 $crate::util::cmd::run_output($binary, [$($arg),*], None)
301 }};
302 // ── default output array ──────────────────────────────────
303 // array + dir
304 ($binary:expr, [ $($arg:expr),* $(,)? ]; dir: $dir:expr) => {{
305 $crate::util::cmd::run_output($binary, [$($arg),*], Some($dir.as_ref()))
306 }};
307 // array (no dir)
308 ($binary:expr, [ $($arg:expr),* $(,)? ]) => {{
309 $crate::util::cmd::run_output($binary, [$($arg),*], None)
310 }};
311 // ── default output variable ───────────────────────────────
312 // variable + dir
313 ($binary:expr, $args:expr; dir: $dir:expr) => {{
314 $crate::util::cmd::run_output($binary, $args, Some($dir.as_ref()))
315 }};
316 // variable (no dir)
317 ($binary:expr, $args:expr) => {{
318 $crate::util::cmd::run_output($binary, $args, None)
319 }};
320}
321/// Build a [`Vec<OsString>`] of command arguments with automatic `.into()` conversion
322/// and `..` spread for conditional sub-vectors.
323///
324/// Eliminates the `[vec!["x".to_string()], vec![y], ..].concat()` pattern by
325/// accepting bare string literals, expressions, and spread iterables in a flat list.
326///
327/// # Syntax
328///
329/// ```ignore
330/// // Literals and variables — auto-converted via Into<OsString>
331/// let args = args!["--name", container_name, "--url", url];
332///
333/// // Spread — injects all items from an IntoIterator
334/// let extra = vec!["--gpus", "all"];
335/// let args = args!["run", "--detach", ..extra, "image"];
336///
337/// // Empty
338/// let args: Vec<OsString> = args![];
339/// ```
340///
341/// Internally composes iterator chains and collects — no mutable state.
342#[macro_export]
343macro_rules! args {
344 // Spread with trailing items
345 (@build $acc:expr; .. $item:expr, $($rest:tt)*) => {
346 $crate::args!(@build ($acc.chain($item.into_iter().map(|s| s.into()))); $($rest)*)
347 };
348 // Spread, last item
349 (@build $acc:expr; .. $item:expr) => {
350 $acc.chain($item.into_iter().map(|s| s.into())).collect::<Vec<std::ffi::OsString>>()
351 };
352 // 2-tuple (key, value) — trailing items follow
353 (@build $acc:expr; ($k:expr, $v:expr), $($rest:tt)*) => {
354 $crate::args!(@build ($acc.chain(core::iter::once($k.into())).chain(core::iter::once($v.into()))); $($rest)*)
355 };
356 // 2-tuple (key, value) — last item
357 (@build $acc:expr; ($k:expr, $v:expr)) => {
358 $acc.chain(core::iter::once($k.into())).chain(core::iter::once($v.into())).collect::<Vec<std::ffi::OsString>>()
359 };
360 // Regular expression, trailing items follow
361 (@build $acc:expr; $item:expr, $($rest:tt)*) => {
362 $crate::args!(@build ($acc.chain(core::iter::once($item.into()))); $($rest)*)
363 };
364 // Regular expression, last item
365 (@build $acc:expr; $item:expr) => {
366 $acc.chain(core::iter::once($item.into())).collect::<Vec<std::ffi::OsString>>()
367 };
368 // Base case — no more tokens
369 (@build $acc:expr;) => {
370 $acc.collect::<Vec<std::ffi::OsString>>()
371 };
372 // Entry point
373 ($($tt:tt)*) => {
374 $crate::args!(@build (core::iter::empty::<std::ffi::OsString>()); $($tt)*)
375 };
376}
377/// Build an analyzer [`Check`] with required category/success and optional fields.
378///
379/// This macro wraps the existing builder API and keeps field assignment explicit.
380/// It accepts any builder method name as a field key (e.g. `severity`, `message`,
381/// `context`, `uri`, `status_code`, `errors`).
382///
383/// # Examples
384///
385/// ```ignore
386/// use acorn::{check, analyzer::{CheckCategory, CheckSeverity}};
387///
388/// let ok = check!(CheckCategory::Prose, true, message: "doc-1");
389/// let err = check!(
390/// CheckCategory::Readability,
391/// false,
392/// severity: CheckSeverity::Warning,
393/// message: "index.json",
394/// context: "12.1",
395/// );
396/// ```
397#[macro_export]
398macro_rules! check {
399 ($category:expr, $success:expr $(, $field:ident : $value:expr )* $(,)?) => {
400 $crate::check!(@apply $crate::analyzer::Check::init().category($category).success($success) $(, $field : $value )*)
401 };
402 (@apply $builder:expr) => {
403 $builder.build()
404 };
405 (@apply $builder:expr, $field:ident : $value:expr $(, $rest_field:ident : $rest_value:expr )* ) => {
406 $crate::check!(@apply $builder.$field($value) $(, $rest_field : $rest_value )*)
407 };
408}
409
410/// Build a successful [`Check`] with optional fields.
411///
412/// # Examples
413///
414/// ```ignore
415/// use acorn::{check_ok, analyzer::CheckCategory};
416///
417/// let check = check_ok!(CheckCategory::Quality, message: "input.json");
418/// ```
419#[macro_export]
420macro_rules! check_ok {
421 ($category:expr $(, $field:ident : $value:expr )* $(,)?) => {
422 $crate::check!($category, true $(, $field : $value )*)
423 };
424}
425
426/// Build a failing [`Check`] with `Error` severity and optional fields.
427///
428/// # Examples
429///
430/// ```ignore
431/// use acorn::{check_err, analyzer::CheckCategory};
432///
433/// let check = check_err!(CheckCategory::Schema, message: "invalid document");
434/// ```
435#[macro_export]
436macro_rules! check_err {
437 ($category:expr $(, $field:ident : $value:expr )* $(,)?) => {
438 $crate::check!($category, false, severity: $crate::analyzer::CheckSeverity::Error $(, $field : $value )*)
439 };
440}
441/// Logging macro for failures
442#[macro_export]
443macro_rules! fail {
444 ($msg:literal, $($rest:tt)*) => {
445 tracing::error!(
446 "{}",
447 format!(
448 "=> {} {}",
449 $crate::util::Label::fail(),
450 format!($msg, $($rest)*)
451 )
452 );
453 };
454 ($msg:literal) => {
455 tracing::error!("{}", format!("=> {} {}", $crate::util::Label::fail(), $msg));
456 };
457 ($($args:tt)*) => {
458 tracing::error!($($args)*);
459 };
460}
461/// Logging macro for skipped operations
462#[macro_export]
463macro_rules! skip {
464 ($msg:literal, $($rest:tt)*) => {
465 tracing::warn!(
466 "{}",
467 format!(
468 "=> {}{}",
469 $crate::util::Label::skip(),
470 format!($msg, $($rest)*)
471 )
472 );
473 };
474 ($msg:literal) => {
475 tracing::warn!("{}", format!("=> {}{}", $crate::util::Label::skip(), $msg));
476 };
477 ($($args:tt)*) => {
478 tracing::warn!($($args)*);
479 };
480}
481/// Creates a `Param` with the given style, name, and values.
482///
483/// The macro supports two calling styles:
484/// - **Ident style** (shorthand): `param!(QueryPair, "q", ...)`
485/// The style is automatically qualified to `ParamStyle::QueryPair`
486/// - **Path style** (explicit): `param!(ParamStyle::QueryPair, "q", ...)`
487/// Use when the full path is preferred or when `ParamStyle` is in scope
488///
489/// # Parameter Styles
490///
491/// Common body type parameters and request styles:
492/// - `Header`: HTTP header (e.g., `"PRIVATE-TOKEN"`)
493/// - `Body`: Request body payload (e.g., JSON or form data)
494/// - `QueryPair`: URL query string parameter
495/// - `FieldList`: Comma-separated field list
496/// - `TemplateValue`: URI template substitution
497/// - `KeyValuePair`: Key-value pair in query or body
498///
499/// # Value Syntaxes
500///
501/// The macro supports four different value syntaxes:
502/// 1. Single value: `param!(FieldList, "fl", "family-name")`
503/// 2. Single tuple: `param!(QueryPair, "filter", ("status", "inactive"))`
504/// 3. Multiple tuples: `param!(QueryPair, "q", (("key1", "val1"), ("key2", "val2")))`
505/// 4. Vec notation: `param!(FieldList, "fields", vec![vec!["field1"], vec!["field2"]])`
506/// 5. Body shorthand: `param!(Body &payload)` (uses an empty key for raw curl `-d` semantics)
507///
508/// # Examples
509///
510/// ```ignore
511/// // Shorthand with single value (ident style)
512/// param!(FieldList, "fl", "family-name")
513///
514/// // Shorthand with single tuple (ident style)
515/// param!(QueryPair, "filter", ("status", "inactive"))
516///
517/// // Shorthand with multiple tuples (ident style)
518/// param!(QueryPair, "q", (
519/// ("affiliation-org-name", "Lyrasis"),
520/// ("ror-org-id", "\"https://ror.org/01qz5mb56\""),
521/// ))
522///
523/// // Path style (explicit ParamStyle reference)
524/// param!(ParamStyle::FieldList, "fl", "family-name")
525///
526/// // KeyValuePair (query string key-value pair)
527/// param!(KeyValuePair, "per_page", "100")
528///
529/// param!(ParamStyle::KeyValuePair, "page", "2")
530///
531/// // Header parameter
532/// param!(Header, "PRIVATE-TOKEN", &token)
533///
534/// // Body shorthand (raw body, no key)
535/// param!(Body &payload)
536///
537/// // Body parameter (with key name)
538/// param!(Body, "body", &payload)
539///
540/// // Vec notation
541/// param!(QueryPair, "q", vec![
542/// vec!["affiliation-org-name", "Lyrasis"],
543/// ])
544/// ```
545#[macro_export]
546macro_rules! param {
547 // Body shorthand without key: param!(Body &payload)
548 (Body $val:expr) => {
549 $crate::io::api::Param::of_type($crate::io::api::ParamStyle::Body)
550 .values(vec![vec![Some($val)]])
551 .with_key("")
552 };
553 // Body shorthand without key (comma form): param!(Body, &payload)
554 (Body, $val:expr) => {
555 $crate::io::api::Param::of_type($crate::io::api::ParamStyle::Body)
556 .values(vec![vec![Some($val)]])
557 .with_key("")
558 };
559 // Ident with multiple tuples: param!(QueryPair, "q", (("a", "b"), ("c", "d")))
560 ($style:ident, $name:expr, ( $( ($($val:expr),* $(,)?) ),+ $(,)? )) => {
561 $crate::io::api::Param::of_type($crate::io::api::ParamStyle::$style)
562 .values(vec![ $( vec![ $( Some($val) ),* ] ),* ])
563 .with_key($name)
564 };
565 // Ident with single tuple: param!(QueryPair, "filter", ("status", "inactive"))
566 ($style:ident, $name:expr, ($($val:expr),+ $(,)?)) => {
567 $crate::io::api::Param::of_type($crate::io::api::ParamStyle::$style)
568 .values(vec![vec![ $( Some($val) ),* ]])
569 .with_key($name)
570 };
571 // Ident with vec notation: param!(FieldList, "fields", vec![vec!["f1"], vec!["f2"]])
572 ($style:ident, $name:expr, vec![ $( vec![ $($val:expr),* $(,)? ] ),* $(,)? ]) => {
573 $crate::io::api::Param::of_type($crate::io::api::ParamStyle::$style)
574 .values(vec![ $( vec![ $( Some($val) ),* ] ),* ])
575 .with_key($name)
576 };
577 // Ident with single value: param!(FieldList, "fl", "family-name")
578 ($style:ident, $name:expr, $val:expr) => {
579 $crate::io::api::Param::of_type($crate::io::api::ParamStyle::$style)
580 .values(vec![vec![Some($val)]])
581 .with_key($name)
582 };
583 // Path with multiple tuples: param!(ParamStyle::QueryPair, "q", (("a", "b"), ("c", "d")))
584 ($style:path, $name:expr, ( $( ($($val:expr),* $(,)?) ),+ $(,)? )) => {
585 $crate::io::api::Param::of_type($style)
586 .values(vec![ $( vec![ $( Some($val) ),* ] ),* ])
587 .with_key($name)
588 };
589 // Path with single tuple: param!(ParamStyle::QueryPair, "filter", ("status", "inactive"))
590 ($style:path, $name:expr, ($($val:expr),+ $(,)?)) => {
591 $crate::io::api::Param::of_type($style)
592 .values(vec![vec![ $( Some($val) ),* ]])
593 .with_key($name)
594 };
595 // Path with vec notation: param!(ParamStyle::FieldList, "fields", vec![vec!["f1"]])
596 ($style:path, $name:expr, vec![ $( vec![ $($val:expr),* $(,)? ] ),* $(,)? ]) => {
597 $crate::io::api::Param::of_type($style)
598 .values(vec![ $( vec![ $( Some($val) ),* ] ),* ])
599 .with_key($name)
600 };
601 // Path with single value: param!(ParamStyle::FieldList, "fl", "family-name")
602 ($style:path, $name:expr, $val:expr) => {
603 $crate::io::api::Param::of_type($style)
604 .values(vec![vec![Some($val)]])
605 .with_key($name)
606 };
607}
608/// Generate a validator function that delegates to a method on the input value
609///
610/// Creates a public function `fn(value: &str) -> Result<(), ValidationError>` that
611/// calls the given method on the input and validates the boolean result.
612///
613/// # Syntax
614///
615/// ```ignore
616/// // Full form: separate function name, method name, error code, and message
617/// method_validator!(
618/// /// Doc comment
619/// function_name,
620/// method_name,
621/// "error_code",
622/// "Error message"
623/// );
624///
625/// // Without message: generates "Provide a valid {code}"
626/// method_validator!(
627/// /// Doc comment
628/// function_name,
629/// method_name,
630/// "error_code"
631/// );
632///
633/// // Shorthand with message: uses function name as method name
634/// method_validator!(
635/// /// Doc comment
636/// function_name,
637/// "error_code",
638/// "Error message"
639/// );
640///
641/// // Minimal: uses function name as method name, generates default message
642/// method_validator!(
643/// /// Doc comment
644/// function_name,
645/// "error_code"
646/// );
647/// ```
648#[macro_export]
649macro_rules! method_validator {
650 ($(#[$meta:meta])* $fn_name:ident, $method:ident, $code:literal, $message:literal) => {
651 #[doc = concat!("Check if value is a valid ", $code)]
652 $(#[$meta])*
653 pub fn $fn_name(value: &str) -> Result<(), ::validator::ValidationError> {
654 match value.$method() {
655 | true => Ok(()),
656 | _ => Err(::validator::ValidationError::new($code).with_message($message.into())),
657 }
658 }
659 };
660 ($(#[$meta:meta])* $fn_name:ident, $method:ident, $code:literal) => {
661 #[doc = concat!("Check if value is a valid ", $code)]
662 $(#[$meta])*
663 pub fn $fn_name(value: &str) -> Result<(), ::validator::ValidationError> {
664 match value.$method() {
665 | true => Ok(()),
666 | _ => Err(::validator::ValidationError::new($code)
667 .with_message(concat!("Provide valid ", $code).into())),
668 }
669 }
670 };
671 ($(#[$meta:meta])* $fn_name:ident, $code:literal, $message:literal) => {
672 #[doc = concat!("Check if value is a valid ", $code)]
673 $(#[$meta])*
674 pub fn $fn_name(value: &str) -> Result<(), ::validator::ValidationError> {
675 match value.$fn_name() {
676 | true => Ok(()),
677 | _ => Err(::validator::ValidationError::new($code).with_message($message.into())),
678 }
679 }
680 };
681 ($(#[$meta:meta])* $fn_name:ident, $code:literal) => {
682 #[doc = concat!("Check if value is a valid ", $code)]
683 $(#[$meta])*
684 pub fn $fn_name(value: &str) -> Result<(), ::validator::ValidationError> {
685 match value.$fn_name() {
686 | true => Ok(()),
687 | _ => Err(::validator::ValidationError::new($code)
688 .with_message(concat!("Provide valid ", $code, " value").into())),
689 }
690 }
691 };
692}
693/// Generate a validator function that matches a value against a regex
694///
695/// Creates a public function `fn(value: &str) -> Result<(), ValidationError>` that
696/// matches the input against the given regex expression.
697///
698/// # Syntax
699/// ```ignore
700/// regex_validator!(
701/// /// Doc comment
702/// function_name,
703/// REGEX_CONSTANT,
704/// "error_code",
705/// "Error message"
706/// );
707/// ```
708#[macro_export]
709macro_rules! regex_validator {
710 ($(#[$meta:meta])* $fn_name:ident, $regex:expr, $code:literal, $message:literal) => {
711 #[doc = concat!("Check if value is a valid ", $code)]
712 $(#[$meta])*
713 pub fn $fn_name(value: &str) -> Result<(), ::validator::ValidationError> {
714 match $regex.is_match(value) {
715 | Ok(value) if value => Ok(()),
716 | _ => Err(::validator::ValidationError::new($code).with_message($message.into())),
717 }
718 }
719 };
720 ($(#[$meta:meta])* $fn_name:ident, $regex:expr, $code:literal) => {
721 #[doc = concat!("Check if value is a valid ", $code)]
722 $(#[$meta])*
723 pub fn $fn_name(value: &str) -> Result<(), ::validator::ValidationError> {
724 match $regex.is_match(value) {
725 | Ok(value) if value => Ok(()),
726 | _ => Err(::validator::ValidationError::new($code)
727 .with_message(concat!("Provide valid ", $code).into())),
728 }
729 }
730 };
731}
732/// Generate a list validator function from an existing scalar validator function
733///
734/// Creates a public function `fn(value: &[String]) -> Result<(), ValidationError>` that
735/// validates each value with the provided scalar validator and returns the first indexed error.
736///
737/// # Syntax
738/// ```ignore
739/// list_validator!(
740/// /// Doc comment
741/// list_function_name,
742/// scalar_function_name,
743/// "error_code",
744/// "Error message"
745/// );
746///
747/// list_validator!(
748/// /// Doc comment
749/// list_function_name,
750/// scalar_function_name,
751/// "error_code"
752/// );
753/// ```
754#[macro_export]
755macro_rules! list_validator {
756 ($(#[$meta:meta])* $fn_name:ident, $validator:ident, $code:literal, $message:literal) => {
757 #[doc = concat!("Check if all values are valid ", $code, " entries")]
758 $(#[$meta])*
759 pub fn $fn_name(value: &[String]) -> Result<(), ::validator::ValidationError> {
760 value
761 .iter()
762 .position(|x| $validator(x).is_err())
763 .map(|index| {
764 let mut err = ::validator::ValidationError::new($code).with_message($message.to_string().into());
765 err.add_param("index".into(), &index);
766 err
767 })
768 .map_or(Ok(()), Err)
769 }
770 };
771 ($(#[$meta:meta])* $fn_name:ident, $validator:ident, $code:literal) => {
772 #[doc = concat!("Check if all values are valid ", $code, " entries")]
773 $(#[$meta])*
774 pub fn $fn_name(value: &[String]) -> Result<(), ::validator::ValidationError> {
775 value
776 .iter()
777 .position(|x| $validator(x).is_err())
778 .map(|index| {
779 let mut err = ::validator::ValidationError::new($code)
780 .with_message(concat!("Every ", $code, " should be valid").to_string().into());
781 err.add_param("index".into(), &index);
782 err
783 })
784 .map_or(Ok(()), Err)
785 }
786 };
787}