use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Arg, ArgAction, Command, value_parser};
use super::{
CliCommand, DbCommand, FrontendArg, InspectTarget, MakeOptions, McpOptions, NewOptions,
OutputFormat, PackageOptions, ReleaseCommand, StubsCommand,
};
pub(crate) fn parse<I, T>(arguments: I) -> Result<CliCommand, clap::Error>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let matches = command().try_get_matches_from(arguments)?;
match matches.subcommand() {
Some(("new", values)) => {
let destination = values
.get_one::<PathBuf>("destination")
.cloned()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::MissingRequiredArgument))?;
let frontend = match values.get_one::<String>("frontend").map(String::as_str) {
Some("vue") => FrontendArg::Vue,
_ => FrontendArg::React,
};
let no_db = values.get_flag("no-db");
Ok(CliCommand::New(NewOptions {
destination,
frontend,
no_db,
}))
}
Some(("install", _)) => Ok(CliCommand::Install),
Some(("dev", _)) => Ok(CliCommand::Dev),
Some(("build", _)) => Ok(CliCommand::Build),
Some(("package", values)) => Ok(CliCommand::Package(parse_package(values))),
Some(("check", values)) => Ok(CliCommand::Check(output_format(values))),
Some(("doctor", values)) => Ok(CliCommand::Doctor {
format: output_format(values),
checks_only: values.get_flag("checks"),
}),
Some(("exposure", values)) => Ok(CliCommand::Exposure(output_format(values))),
Some(("routes", values)) => Ok(CliCommand::Routes(output_format(values))),
Some(("modules", values)) => Ok(CliCommand::Modules(output_format(values))),
Some(("services", values)) => Ok(CliCommand::Services(output_format(values))),
Some(("schedule", values)) => Ok(CliCommand::Schedule(output_format(values))),
Some(("run", values)) => {
let name = values
.get_one::<String>("name")
.cloned()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::MissingRequiredArgument))?;
let arguments = values
.get_many::<String>("arguments")
.map(|items| items.cloned().collect())
.unwrap_or_default();
Ok(CliCommand::Run { name, arguments })
}
Some(("make", values)) => {
let kind = values
.get_one::<String>("kind")
.cloned()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::MissingRequiredArgument))?;
let name = values
.get_one::<String>("name")
.cloned()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::MissingRequiredArgument))?;
let module = values.get_one::<String>("module").cloned();
let dry_run = values.get_flag("dry-run");
let force = values.get_flag("force");
Ok(CliCommand::Make(MakeOptions {
kind,
name,
module,
dry_run,
force,
}))
}
Some(("stubs", sub)) => parse_stubs(sub),
Some(("s", values)) => {
let name = values
.get_one::<String>("name")
.cloned()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::MissingRequiredArgument))?;
let arguments = values
.get_many::<String>("arguments")
.map(|items| items.cloned().collect())
.unwrap_or_default();
Ok(CliCommand::Script { name, arguments })
}
Some(("db", sub)) => parse_db(sub),
Some(("release", sub)) => parse_release(sub),
Some(("inspect", sub)) => parse_inspect(sub),
Some(("mcp", values)) => parse_mcp(values),
_ => Err(clap::Error::new(clap::error::ErrorKind::MissingSubcommand)),
}
}
fn parse_db(sub: &clap::ArgMatches) -> Result<CliCommand, clap::Error> {
match sub.subcommand() {
Some(("migrate", _)) => Ok(CliCommand::Db(DbCommand::Migrate)),
Some(("rollback", values)) => {
let steps = values.get_one::<u32>("steps").copied();
Ok(CliCommand::Db(DbCommand::Rollback { steps }))
}
Some(("status", _)) => Ok(CliCommand::Db(DbCommand::Status)),
Some(("fresh", values)) => Ok(CliCommand::Db(DbCommand::Fresh {
force: values.get_flag("force"),
})),
Some(("reset", values)) => Ok(CliCommand::Db(DbCommand::Reset {
force: values.get_flag("force"),
})),
Some(("refresh", values)) => Ok(CliCommand::Db(DbCommand::Refresh {
force: values.get_flag("force"),
})),
Some(("prepare", values)) => {
if values.get_flag("check") {
Ok(CliCommand::Db(DbCommand::PrepareCheck))
} else {
Ok(CliCommand::Db(DbCommand::Prepare))
}
}
Some(("lint", values)) => Ok(CliCommand::Db(DbCommand::Lint {
format: output_format(values),
})),
_ => Err(clap::Error::new(clap::error::ErrorKind::MissingSubcommand)),
}
}
fn parse_release(sub: &clap::ArgMatches) -> Result<CliCommand, clap::Error> {
match sub.subcommand() {
Some(("validate", values)) => Ok(CliCommand::Release(ReleaseCommand::Validate(
output_format(values),
))),
Some(("changes", sub)) => parse_release_changes(sub),
Some(("graph", values)) => Ok(CliCommand::Release(ReleaseCommand::Graph(output_format(
values,
)))),
Some(("version", values)) => Ok(CliCommand::Release(ReleaseCommand::Version(
output_format(values),
))),
Some(("plan", values)) => Ok(CliCommand::Release(ReleaseCommand::Plan(output_format(
values,
)))),
Some(("prepare", values)) => Ok(CliCommand::Release(ReleaseCommand::Prepare {
format: output_format(values),
dry_run: values.get_flag("dry-run"),
})),
Some(("publish", values)) => Ok(CliCommand::Release(ReleaseCommand::Publish {
format: output_format(values),
dry_run: values.get_flag("dry-run"),
})),
Some(("platform", sub)) => parse_release_platform(sub),
_ => Err(clap::Error::new(clap::error::ErrorKind::MissingSubcommand)),
}
}
fn parse_release_platform(sub: &clap::ArgMatches) -> Result<CliCommand, clap::Error> {
match sub.subcommand() {
Some(("validate", values)) => {
let platform = values
.get_one::<String>("platform")
.cloned()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::MissingRequiredArgument))?;
Ok(CliCommand::Release(ReleaseCommand::PlatformValidate {
format: output_format(values),
platform,
}))
}
_ => Err(clap::Error::new(clap::error::ErrorKind::MissingSubcommand)),
}
}
fn parse_release_changes(sub: &clap::ArgMatches) -> Result<CliCommand, clap::Error> {
match sub.subcommand() {
Some(("validate", values)) => Ok(CliCommand::Release(ReleaseCommand::ChangesValidate(
output_format(values),
))),
_ => Err(clap::Error::new(clap::error::ErrorKind::MissingSubcommand)),
}
}
fn parse_stubs(sub: &clap::ArgMatches) -> Result<CliCommand, clap::Error> {
match sub.subcommand() {
Some(("publish", values)) => Ok(CliCommand::Stubs(StubsCommand::Publish {
dry_run: values.get_flag("dry-run"),
force: values.get_flag("force"),
})),
_ => Err(clap::Error::new(clap::error::ErrorKind::MissingSubcommand)),
}
}
fn parse_inspect(sub: &clap::ArgMatches) -> Result<CliCommand, clap::Error> {
match sub.subcommand() {
Some(("app", values)) => Ok(CliCommand::Inspect(InspectTarget::App(output_format(
values,
)))),
Some(("route", values)) => {
let name = values
.get_one::<String>("name")
.cloned()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::MissingRequiredArgument))?;
Ok(CliCommand::Inspect(InspectTarget::Route {
name,
format: output_format(values),
}))
}
Some(("model", values)) => {
let name = values
.get_one::<String>("name")
.cloned()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::MissingRequiredArgument))?;
Ok(CliCommand::Inspect(InspectTarget::Module {
name,
format: output_format(values),
}))
}
_ => Err(clap::Error::new(clap::error::ErrorKind::MissingSubcommand)),
}
}
fn parse_mcp(values: &clap::ArgMatches) -> Result<CliCommand, clap::Error> {
Ok(CliCommand::Mcp(McpOptions {
allow_destructive_writes: values.get_flag("allow-destructive-writes"),
}))
}
fn command() -> Command {
Command::new("arc")
.version(env!("CARGO_PKG_VERSION"))
.about("Develop and operate Arcature applications")
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new("new")
.about("Create an Arcature application")
.arg(
Arg::new("destination")
.required(true)
.help("Directory to create; its final component becomes the project name")
.value_parser(value_parser!(PathBuf)),
)
.arg(
Arg::new("frontend")
.long("frontend")
.value_name("react|vue")
.help("Frontend framework")
.default_value("react")
.value_parser(["react", "vue"]),
)
.arg(
Arg::new("no-db")
.long("no-db")
.help("Generate a project without the database subsystem")
.action(ArgAction::SetTrue),
),
)
.subcommand(Command::new("install").about("Install locked frontend dependencies"))
.subcommand(Command::new("dev").about("Run the full development environment"))
.subcommand(Command::new("build").about("Build frontend and backend for production"))
.subcommand(
Command::new("package")
.about("Assemble a production artifact bundle at dist/<app-target>/")
.arg(
Arg::new("no-build")
.long("no-build")
.help("Skip the precondition `arc build` (assume build output is fresh)")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("target-label")
.long("target-label")
.help("Override the output target name (defaults to <app>-<host-target>)"),
),
)
.subcommand(report_command("check", "Check project health"))
.subcommand(
Command::new("doctor")
.about("Inspect the local Arcature environment")
.arg(
Arg::new("json")
.long("json")
.help("Emit stable machine-readable JSON")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("checks")
.long("checks")
.help("Run only the AP2.1-7 system-check framework (stable IDs, severity, fix hints)")
.action(ArgAction::SetTrue),
),
)
.subcommand(report_command(
"exposure",
"List browser-exposed page contracts and lint secret-bearing fields",
))
.subcommand(report_command(
"routes",
"List all application routes (side-effect-free, no boot)",
))
.subcommand(report_command(
"modules",
"List application modules and their bindings (side-effect-free, no boot)",
))
.subcommand(report_command(
"services",
"List application services and their dependencies (side-effect-free, no boot)",
))
.subcommand(report_command(
"schedule",
"List scheduled jobs and their cadence (side-effect-free, no boot)",
))
.subcommand(
Command::new("inspect")
.about("Inspect the Unified Application Graph (side-effect-free, no boot)")
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(report_command(
"app",
"Inspect the whole application graph",
))
.subcommand(
Command::new("route")
.about("Inspect a single route by its dotted name")
.arg(
Arg::new("name")
.required(true)
.help("Route name (e.g. links.show)"),
)
.arg(
Arg::new("json")
.long("json")
.help("Emit stable machine-readable JSON")
.action(ArgAction::SetTrue),
),
)
.subcommand(
Command::new("model")
.about("Inspect a single module by name")
.arg(
Arg::new("name")
.required(true)
.help("Module name (e.g. Links)"),
)
.arg(
Arg::new("json")
.long("json")
.help("Emit stable machine-readable JSON")
.action(ArgAction::SetTrue),
),
),
)
.subcommand(
Command::new("run")
.about("Run a compiled application command by name")
.arg(
Arg::new("name")
.required(true)
.help("Command name (e.g. users:prune)"),
)
.arg(
Arg::new("arguments")
.help("Arguments forwarded to the command")
.num_args(0..)
.last(true)
.action(ArgAction::Append),
),
)
.subcommand(
Command::new("make")
.about("Generate a source file for the given kind")
.arg(
Arg::new("kind")
.required(true)
.help("Generator kind: module, controller, request, service, policy, middleware, event, listener, job, command, test, mail, resource"),
)
.arg(
Arg::new("name")
.required(true)
.help("Name of the item to generate (e.g. Links, send_welcome)"),
)
.arg(
Arg::new("module")
.long("module")
.value_name("module")
.help("Module directory to place the file in (e.g. links)"),
)
.arg(
Arg::new("dry-run")
.long("dry-run")
.help("Report what would happen without writing files")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("force")
.long("force")
.help("Explicitly overwrite an existing file (default: refuse)")
.action(ArgAction::SetTrue),
),
)
.subcommand(
Command::new("stubs")
.about("Publish real stub/scaffolding files into the application (AP2.1-11)")
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new("publish")
.about("Publish stub/scaffolding files into the project")
.arg(
Arg::new("dry-run")
.long("dry-run")
.help("Show what would happen without writing files")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("force")
.long("force")
.help("Explicitly overwrite existing stub files")
.action(ArgAction::SetTrue),
),
),
)
.subcommand(
Command::new("s")
.about("Run an application-owned script")
.arg(
Arg::new("name")
.required(true)
.help("Script name from the project's s.script file"),
)
.arg(
Arg::new("arguments")
.help("Arguments forwarded as distinct argv values")
.num_args(0..)
.last(true)
.action(ArgAction::Append),
),
)
.subcommand(db_command())
.subcommand(release_command())
.subcommand(
Command::new("mcp")
.about("Run the MCP (Model Context Protocol) server over stdio (AP2.1-9)")
.arg(
Arg::new("allow-destructive-writes")
.long("allow-destructive-writes")
.help(
"Enable the destructive-write capability (off by default). \
Read-only tools need no capabilities; this flags the boundary \
future gated destructive tools require. Shell is never available.",
)
.action(ArgAction::SetTrue),
),
)
}
fn db_command() -> Command {
Command::new("db")
.about("Database lifecycle: migrations, status, and schema preparation")
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(Command::new("migrate").about("Apply pending database migrations"))
.subcommand(
Command::new("rollback")
.about("Roll back applied database migrations")
.arg(
Arg::new("steps")
.long("steps")
.help("Number of migrations to roll back (default: all)")
.value_parser(value_parser!(u32)),
),
)
.subcommand(Command::new("status").about("Show migration status"))
.subcommand(destructive_command(
"fresh",
"Drop all tables and reapply all migrations",
))
.subcommand(destructive_command("reset", "Roll back all migrations"))
.subcommand(destructive_command(
"refresh",
"Roll back all, then reapply all migrations",
))
.subcommand(
Command::new("prepare")
.about("Generate SQLx offline metadata (cargo sqlx prepare --workspace)")
.arg(
Arg::new("check")
.long("check")
.help("Check offline metadata is current instead of generating")
.action(ArgAction::SetTrue),
),
)
.subcommand(
Command::new("lint")
.about("Classify migration SQL for PostgreSQL risks (reads stdin, no DB)")
.arg(
Arg::new("json")
.long("json")
.help("Emit stable machine-readable JSON")
.action(ArgAction::SetTrue),
),
)
}
fn destructive_command(name: &'static str, about: &'static str) -> Command {
Command::new(name).about(about).arg(
Arg::new("force")
.long("force")
.help("Confirm the destructive operation (required)")
.action(ArgAction::SetTrue),
)
}
fn release_command() -> Command {
Command::new("release")
.about("Arcature release lifecycle: metadata validation, planning, and publishing")
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new("validate")
.about(
"Validate [package.metadata.arcature] across the workspace (side-effect-free)",
)
.arg(
Arg::new("json")
.long("json")
.help("Emit stable machine-readable JSON")
.action(ArgAction::SetTrue),
),
)
.subcommand(
Command::new("changes")
.about("Change fragments: release-intent declarations under changes/")
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new("validate")
.about("Validate change fragments (side-effect-free)")
.arg(
Arg::new("json")
.long("json")
.help("Emit stable machine-readable JSON")
.action(ArgAction::SetTrue),
),
),
)
.subcommand(
Command::new("graph")
.about(
"Show the publishable dependency graph and topological order (side-effect-free)",
)
.arg(
Arg::new("json")
.long("json")
.help("Emit stable machine-readable JSON")
.action(ArgAction::SetTrue),
),
)
.subcommand(
Command::new("version")
.about(
"Show current YBF versions and validate sibling dependency ranges (side-effect-free)",
)
.arg(
Arg::new("json")
.long("json")
.help("Emit stable machine-readable JSON")
.action(ArgAction::SetTrue),
),
)
.subcommand(
Command::new("plan")
.about(
"Compute and display a proposed Release Plan (side-effect-free)",
)
.arg(
Arg::new("json")
.long("json")
.help("Emit stable machine-readable JSON")
.action(ArgAction::SetTrue),
),
)
.subcommand(
Command::new("prepare")
.about(
"Materialize a committed Release Plan: manifest edits, plan TOML, fragment archive",
)
.arg(
Arg::new("json")
.long("json")
.help("Emit stable machine-readable JSON")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("dry-run")
.long("dry-run")
.help("Show what would happen without writing files")
.action(ArgAction::SetTrue),
),
)
.subcommand(
Command::new("publish")
.about(
"Execute a committed Release Plan: selective topological publish, \
idempotent resume, create crate version tags",
)
.arg(
Arg::new("json")
.long("json")
.help("Emit stable machine-readable JSON")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("dry-run")
.long("dry-run")
.help("Show what would happen without publishing or creating tags")
.action(ArgAction::SetTrue),
),
)
.subcommand(
Command::new("platform")
.about("Platform manifest: the Certified Stack Contract")
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new("validate")
.about("Validate a Platform manifest against the workspace")
.arg(
Arg::new("platform")
.long("platform")
.help("Platform version (e.g. 2026.1)")
.required(true),
)
.arg(
Arg::new("json")
.long("json")
.help("Emit stable machine-readable JSON")
.action(ArgAction::SetTrue),
),
),
)
}
fn report_command(name: &'static str, about: &'static str) -> Command {
Command::new(name).about(about).arg(
Arg::new("json")
.long("json")
.help("Emit stable machine-readable JSON")
.action(ArgAction::SetTrue),
)
}
fn output_format(matches: &clap::ArgMatches) -> OutputFormat {
if matches.get_flag("json") {
OutputFormat::Json
} else {
OutputFormat::Human
}
}
fn parse_package(values: &clap::ArgMatches) -> PackageOptions {
let no_build = values.get_flag("no-build");
let target_label = values
.get_one::<String>("target-label")
.map(String::to_owned);
PackageOptions {
no_build,
target_label,
}
}
#[cfg(test)]
mod tests {
use clap::error::ErrorKind;
use super::parse;
use crate::cli::{
CliCommand, DbCommand, FrontendArg, InspectTarget, OutputFormat, PackageOptions,
ReleaseCommand,
};
#[test]
fn parses_default_and_explicit_frontends() {
let react = parse(["arc", "new", "demo"]).expect("React command should parse");
let vue =
parse(["arc", "new", "demo", "--frontend", "vue"]).expect("Vue command should parse");
assert!(
matches!(react, CliCommand::New(options) if options.frontend == FrontendArg::React)
);
assert!(matches!(vue, CliCommand::New(options) if options.frontend == FrontendArg::Vue));
}
#[test]
fn parses_install_command() {
assert_eq!(
parse(["arc", "install"]).expect("install should parse"),
CliCommand::Install
);
}
#[test]
fn parses_no_db_flag() {
let no_db = parse(["arc", "new", "demo", "--no-db"]).expect("no-db should parse");
assert!(matches!(no_db, CliCommand::New(options) if options.no_db));
let default = parse(["arc", "new", "demo"]).expect("default should parse");
assert!(matches!(default, CliCommand::New(options) if !options.no_db));
}
#[test]
fn rejects_unknown_frontend_and_command() {
let frontend = parse(["arc", "new", "demo", "--frontend", "svelte"])
.expect_err("unsupported frontend should fail");
let command = parse(["arc", "deploy"]).expect_err("unknown command should fail");
assert_eq!(frontend.kind(), ErrorKind::InvalidValue);
assert_eq!(command.kind(), ErrorKind::InvalidSubcommand);
}
#[test]
fn reports_help_version_and_missing_arguments() {
assert_eq!(
parse(["arc", "--help"]).expect_err("help exits").kind(),
ErrorKind::DisplayHelp
);
assert_eq!(
parse(["arc", "--version"])
.expect_err("version exits")
.kind(),
ErrorKind::DisplayVersion
);
assert_eq!(
parse(["arc", "new"])
.expect_err("destination is required")
.kind(),
ErrorKind::MissingRequiredArgument
);
}
#[test]
fn preserves_forwarded_script_arguments() {
let parsed = parse([
"arc",
"s",
"user:add",
"--",
"alice@example.com",
";touch /tmp/no",
])
.expect("script command should parse");
assert!(
matches!(parsed, CliCommand::Script { name, arguments } if name == "user:add" && arguments == ["alice@example.com", ";touch /tmp/no"])
);
}
#[test]
fn parses_machine_readable_reports() {
assert_eq!(
parse(["arc", "doctor", "--json"]).expect("doctor should parse"),
CliCommand::Doctor {
format: OutputFormat::Json,
checks_only: false,
}
);
assert_eq!(
parse(["arc", "check"]).expect("check should parse"),
CliCommand::Check(OutputFormat::Human)
);
}
#[test]
fn parses_doctor_checks_flag() {
assert_eq!(
parse(["arc", "doctor", "--checks"]).expect("doctor --checks should parse"),
CliCommand::Doctor {
format: OutputFormat::Human,
checks_only: true,
}
);
assert_eq!(
parse(["arc", "doctor", "--checks", "--json"])
.expect("doctor --checks --json should parse"),
CliCommand::Doctor {
format: OutputFormat::Json,
checks_only: true,
}
);
assert_eq!(
parse(["arc", "doctor"]).expect("doctor should parse"),
CliCommand::Doctor {
format: OutputFormat::Human,
checks_only: false,
}
);
}
#[test]
fn parses_exposure_command() {
let human = parse(["arc", "exposure"]).expect("exposure should parse");
let json = parse(["arc", "exposure", "--json"]).expect("exposure --json should parse");
assert_eq!(human, CliCommand::Exposure(OutputFormat::Human));
assert_eq!(json, CliCommand::Exposure(OutputFormat::Json));
}
#[test]
fn parses_inspection_reports() {
assert_eq!(
parse(["arc", "routes"]).expect("routes should parse"),
CliCommand::Routes(OutputFormat::Human)
);
assert_eq!(
parse(["arc", "routes", "--json"]).expect("routes --json should parse"),
CliCommand::Routes(OutputFormat::Json)
);
assert_eq!(
parse(["arc", "modules"]).expect("modules should parse"),
CliCommand::Modules(OutputFormat::Human)
);
assert_eq!(
parse(["arc", "services", "--json"]).expect("services --json should parse"),
CliCommand::Services(OutputFormat::Json)
);
assert_eq!(
parse(["arc", "schedule"]).expect("schedule should parse"),
CliCommand::Schedule(OutputFormat::Human)
);
}
#[test]
fn parses_run_command_with_arguments() {
let parsed = parse(["arc", "run", "users:prune"]).expect("run should parse");
assert!(
matches!(parsed, CliCommand::Run { name, arguments } if name == "users:prune" && arguments.is_empty())
);
let parsed = parse(["arc", "run", "db:cleanup", "--", "--dry-run", "30d"])
.expect("run with args should parse");
assert!(
matches!(parsed, CliCommand::Run { name, arguments } if name == "db:cleanup" && arguments == ["--dry-run", "30d"])
);
}
#[test]
fn parses_make_command_with_and_without_module() {
let parsed = parse(["arc", "make", "controller", "Sessions"]).expect("make should parse");
assert!(
matches!(parsed, CliCommand::Make(opts) if opts.kind == "controller" && opts.name == "Sessions" && opts.module.is_none())
);
let parsed = parse(["arc", "make", "request", "Login", "--module", "accounts"])
.expect("make with module should parse");
assert!(
matches!(parsed, CliCommand::Make(opts) if opts.kind == "request" && opts.name == "Login" && opts.module.as_deref() == Some("accounts"))
);
}
#[test]
fn run_requires_name() {
let err = parse(["arc", "run"]).expect_err("run needs a name");
assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
}
#[test]
fn make_requires_kind_and_name() {
let err = parse(["arc", "make"]).expect_err("make needs kind + name");
assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
}
#[test]
fn parses_db_subcommands() {
assert_eq!(
parse(["arc", "db", "migrate"]).expect("migrate should parse"),
CliCommand::Db(DbCommand::Migrate),
);
assert_eq!(
parse(["arc", "db", "status"]).expect("status should parse"),
CliCommand::Db(DbCommand::Status),
);
assert_eq!(
parse(["arc", "db", "rollback"]).expect("rollback default should parse"),
CliCommand::Db(DbCommand::Rollback { steps: None }),
);
assert_eq!(
parse(["arc", "db", "rollback", "--steps", "3"]).expect("rollback steps should parse"),
CliCommand::Db(DbCommand::Rollback { steps: Some(3) }),
);
}
#[test]
fn parses_db_destructive_commands_require_force() {
let fresh = parse(["arc", "db", "fresh"]).expect("fresh should parse");
assert!(matches!(
fresh,
CliCommand::Db(DbCommand::Fresh { force: false })
));
let fresh_forced =
parse(["arc", "db", "fresh", "--force"]).expect("fresh --force should parse");
assert!(matches!(
fresh_forced,
CliCommand::Db(DbCommand::Fresh { force: true })
));
}
#[test]
fn parses_db_prepare_and_prepare_check() {
let prepare = parse(["arc", "db", "prepare"]).expect("prepare should parse");
assert!(matches!(prepare, CliCommand::Db(DbCommand::Prepare)));
let check =
parse(["arc", "db", "prepare", "--check"]).expect("prepare --check should parse");
assert!(matches!(check, CliCommand::Db(DbCommand::PrepareCheck)));
}
#[test]
fn parses_db_lint_human_and_json() {
let human = parse(["arc", "db", "lint"]).expect("db lint should parse");
assert!(matches!(
human,
CliCommand::Db(DbCommand::Lint {
format: OutputFormat::Human
})
));
let json = parse(["arc", "db", "lint", "--json"]).expect("db lint --json should parse");
assert!(matches!(
json,
CliCommand::Db(DbCommand::Lint {
format: OutputFormat::Json
})
));
}
#[test]
fn parses_release_validate_human_and_json() {
let human = parse(["arc", "release", "validate"]).expect("release validate should parse");
assert_eq!(
human,
CliCommand::Release(ReleaseCommand::Validate(OutputFormat::Human))
);
let json = parse(["arc", "release", "validate", "--json"])
.expect("release validate --json should parse");
assert_eq!(
json,
CliCommand::Release(ReleaseCommand::Validate(OutputFormat::Json))
);
}
#[test]
fn release_requires_subcommand() {
let err = parse(["arc", "release"]).expect_err("release needs a subcommand");
assert_eq!(
err.kind(),
ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
);
}
#[test]
fn parses_inspect_app() {
let parsed = parse(["arc", "inspect", "app"]).expect("inspect app should parse");
assert_eq!(
parsed,
CliCommand::Inspect(InspectTarget::App(OutputFormat::Human))
);
let json = parse(["arc", "inspect", "app", "--json"]).expect("inspect app --json");
assert_eq!(
json,
CliCommand::Inspect(InspectTarget::App(OutputFormat::Json))
);
}
#[test]
fn parses_inspect_route_and_module() {
let route =
parse(["arc", "inspect", "route", "links.show"]).expect("inspect route should parse");
assert_eq!(
route,
CliCommand::Inspect(InspectTarget::Route {
name: "links.show".to_owned(),
format: OutputFormat::Human,
})
);
let model =
parse(["arc", "inspect", "model", "Links"]).expect("inspect model should parse");
assert_eq!(
model,
CliCommand::Inspect(InspectTarget::Module {
name: "Links".to_owned(),
format: OutputFormat::Human,
})
);
let route_json = parse(["arc", "inspect", "route", "links.show", "--json"])
.expect("inspect route --json should parse");
assert_eq!(
route_json,
CliCommand::Inspect(InspectTarget::Route {
name: "links.show".to_owned(),
format: OutputFormat::Json,
})
);
}
#[test]
fn inspect_route_requires_name() {
let err = parse(["arc", "inspect", "route"]).expect_err("route needs a name");
assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
}
#[test]
fn inspect_requires_subcommand() {
let err = parse(["arc", "inspect"]).expect_err("inspect needs a subcommand");
assert_eq!(
err.kind(),
ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
);
}
#[test]
fn parses_package_defaults_and_flags() {
let parsed = parse(["arc", "package"]).expect("package should parse");
assert!(matches!(
parsed,
CliCommand::Package(PackageOptions {
no_build: false,
target_label: None,
})
));
let parsed = parse(["arc", "package", "--no-build"]).expect("package --no-build");
assert!(matches!(
parsed,
CliCommand::Package(PackageOptions {
no_build: true,
target_label: None,
})
));
let parsed = parse(["arc", "package", "--target-label", "demo-prod"])
.expect("package --target-label");
assert!(matches!(
parsed,
CliCommand::Package(PackageOptions {
no_build: false,
target_label: Some(ref label),
}) if label == "demo-prod"
));
}
}