Skip to main content

haz_cli/cli/
check.rs

1//! Parser for the `haz check` subcommand.
2//!
3//! Owns [`CheckArgs`] and the [`check_command`] parser builder.
4//! `haz check` accepts only the `AUX-009` scope flags; their
5//! mutual exclusion is enforced at parse time.
6
7use bpaf::{Parser, construct, long};
8
9use crate::cli::Scope;
10
11/// Arguments parsed for the `haz check` subcommand.
12///
13/// `haz check` accepts only the scope flags (`--all`,
14/// `--workspace`) per `AUX-009`. Like `haz run`, the two scope
15/// flags are mutually exclusive; the parser reduces them to a
16/// single [`Scope`] so any [`CheckArgs`] value reaching the
17/// executor is in a well-formed shape.
18#[derive(Debug, Clone, PartialEq, Eq, Default)]
19pub struct CheckArgs {
20    /// How broadly to validate (`AUX-009`): the bearing project
21    /// or whole workspace ([`Scope::Default`]), every task in the
22    /// bearing scope ([`Scope::All`], mirroring `EXEC-024`), or
23    /// every task in every project ([`Scope::Workspace`],
24    /// mirroring `EXEC-025`). The `--all` / `--workspace` mutex is
25    /// reduced from the two switches at parse time.
26    pub scope: Scope,
27}
28
29pub(super) fn check_command() -> impl Parser<CheckArgs> {
30    let all = long("all")
31        .help("Validate every task in the bearing scope (AUX-009, mirrors EXEC-024)")
32        .switch();
33    let workspace = long("workspace")
34        .help("Validate every task in every project, regardless of cwd (AUX-009, mirrors EXEC-025)")
35        .switch();
36    construct!(all, workspace)
37        // AUX-009 scope mutex. Caught at parse time so the
38        // executor never sees both flags set.
39        .parse(|(all, workspace)| {
40            Scope::from_flags(all, workspace)
41                .map(|scope| CheckArgs { scope })
42                .ok_or_else(|| "`--all` and `--workspace` are mutually exclusive".to_owned())
43        })
44}
45
46#[cfg(test)]
47mod tests {
48    use super::CheckArgs;
49    use crate::cli::{Cmd, Scope, parse};
50
51    #[test]
52    fn aux_009_check_with_no_args_parses() {
53        assert_eq!(parse(&["check"]).unwrap(), Cmd::Check(CheckArgs::default()),);
54    }
55
56    #[test]
57    fn aux_009_check_with_all_flag_parses() {
58        assert_eq!(
59            parse(&["check", "--all"]).unwrap(),
60            Cmd::Check(CheckArgs { scope: Scope::All }),
61        );
62    }
63
64    #[test]
65    fn aux_009_check_with_workspace_flag_parses() {
66        assert_eq!(
67            parse(&["check", "--workspace"]).unwrap(),
68            Cmd::Check(CheckArgs {
69                scope: Scope::Workspace,
70            }),
71        );
72    }
73
74    #[test]
75    fn aux_009_check_all_plus_workspace_rejects() {
76        // AUX-009 scope mutex (mirrors EXEC-024 / EXEC-025).
77        let err = parse(&["check", "--all", "--workspace"]).unwrap_err();
78        assert!(
79            err.contains("mutually exclusive"),
80            "diagnostic should name the mutex: {err}",
81        );
82    }
83
84    #[test]
85    fn aux_009_check_rejects_unknown_flag() {
86        // AUX-009 form contract: no positional or extra flags
87        // beyond --all / --workspace.
88        let err = parse(&["check", "--dry-run"]).unwrap_err();
89        assert!(
90            !err.is_empty(),
91            "unknown flag must produce a diagnostic: {err}",
92        );
93    }
94
95    #[test]
96    fn aux_009_check_rejects_positional() {
97        // AUX-009 form contract: haz check takes no positional
98        // arguments.
99        let err = parse(&["check", "lib:build"]).unwrap_err();
100        assert!(
101            !err.is_empty(),
102            "positional argument must produce a diagnostic: {err}",
103        );
104    }
105}