Skip to main content

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}