diff --git a/docs/netsuke-design.md b/docs/netsuke-design.md index 020277d2..3b36d4fe 100644 --- a/docs/netsuke-design.md +++ b/docs/netsuke-design.md @@ -1348,13 +1348,13 @@ entire CLI specification. Rust ```rust -use clap::{Parser, Subcommand}; - +use clap::{Args, Parser, Subcommand}; use std::path::PathBuf; #[derive(Parser)] #[command(author, version, about, long_about = None)] -struct Cli { /// Path to the Netsuke manifest file to use. +struct Cli { + /// Path to the Netsuke manifest file to use. #[arg(short, long, value_name = "FILE", default_value = "Netsukefile")] file: PathBuf, @@ -1371,26 +1371,38 @@ struct Cli { /// Path to the Netsuke manifest file to use. verbose: bool, #[command(subcommand)] - command: Option, } + command: Option, +} #[derive(Subcommand)] -enum Commands { /// Build specified targets (or default targets if none are -given) [default]. Build { /// Write the generated Ninja manifest to this path -and retain it. - #[arg(long, value_name = "FILE")] - emit: Option, +enum Commands { + /// Build specified targets (or default targets if none are given). + /// This is the default subcommand. + Build(BuildArgs), - /// A list of specific targets to build. targets: Vec, }, - - /// Remove build artefacts and intermediate files. Clean {}, + /// Remove build artefacts and intermediate files. + Clean, /// Display the build dependency graph in DOT format for visualisation. - Graph {}, + Graph, - /// Write the Ninja manifest to `FILE` without invoking Ninja. Manifest { - /// Output path for the generated Ninja file. + /// Write the Ninja manifest to `FILE` without invoking Ninja. + Manifest { + /// Output path for the generated Ninja file. #[arg(value_name = "FILE")] - file: PathBuf, }, } + file: PathBuf, + }, +} + +#[derive(Args)] +struct BuildArgs { + /// Write the generated Ninja manifest to this path and retain it. + #[arg(long, value_name = "FILE")] + emit: Option, + + /// A list of specific targets to build. + targets: Vec, +} ``` *Note: The* `Build` *command is wrapped in an* `Option` *and will be diff --git a/src/cli.rs b/src/cli.rs index aac87248..06c315b8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,7 +3,7 @@ //! This module defines the [`Cli`] structure and its subcommands. //! It mirrors the design described in `docs/netsuke-design.md`. -use clap::{Parser, Subcommand}; +use clap::{Args, Parser, Subcommand}; use std::path::PathBuf; /// Maximum number of jobs accepted by the CLI. @@ -71,27 +71,31 @@ impl Cli { #[must_use] fn with_default_command(mut self) -> Self { if self.command.is_none() { - self.command = Some(Commands::Build { + self.command = Some(Commands::Build(BuildArgs { emit: None, targets: Vec::new(), - }); + })); } self } } +/// Arguments accepted by the `build` command. +#[derive(Debug, Args, PartialEq, Eq, Clone)] +pub struct BuildArgs { + /// Write the generated Ninja manifest to this path and retain it. + #[arg(long, value_name = "FILE")] + pub emit: Option, + + /// A list of specific targets to build. + pub targets: Vec, +} + /// Available top-level commands for Netsuke. #[derive(Debug, Subcommand, PartialEq, Eq, Clone)] pub enum Commands { /// Build specified targets (or default targets if none are given) [default]. - Build { - /// Write the generated Ninja manifest to this path and retain it. - #[arg(long, value_name = "FILE")] - emit: Option, - - /// A list of specific targets to build. - targets: Vec, - }, + Build(BuildArgs), /// Remove build artifacts and intermediate files. Clean, diff --git a/src/runner.rs b/src/runner.rs index 5ef6786d..aca4d7c1 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -4,7 +4,7 @@ //! handles command execution. It now delegates build requests to the Ninja //! subprocess, streaming its output back to the user. -use crate::cli::{Cli, Commands}; +use crate::cli::{BuildArgs, Cli, Commands}; use crate::{ir::BuildGraph, manifest, ninja_gen}; use anyhow::{Context, Result}; use serde_json; @@ -69,15 +69,15 @@ impl BuildTargets { /// /// Returns an error if manifest generation or the Ninja process fails. pub fn run(cli: &Cli) -> Result<()> { - let command = cli.command.clone().unwrap_or(Commands::Build { + let command = cli.command.clone().unwrap_or(Commands::Build(BuildArgs { emit: None, targets: Vec::new(), - }); + })); match command { - Commands::Build { targets, emit } => { + Commands::Build(args) => { let ninja = generate_ninja(cli)?; - let targets = BuildTargets::new(targets); - if let Some(path) = emit { + let targets = BuildTargets::new(args.targets); + if let Some(path) = args.emit { write_and_log(&path, &ninja)?; run_ninja(Path::new("ninja"), cli, &path, &targets)?; } else { @@ -140,7 +140,7 @@ fn write_and_log(path: &Path, content: &NinjaContent) -> Result<()> { /// directory: None, /// jobs: None, /// verbose: false, -/// command: Some(Commands::Build { emit: None, targets: vec![] }), +/// command: Some(Commands::Build(BuildArgs { emit: None, targets: vec![] })), /// }; /// let ninja = generate_ninja(&cli).expect("generate"); /// assert!(ninja.as_str().contains("rule")); diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs index b6999f8b..39284a2f 100644 --- a/tests/cli_tests.rs +++ b/tests/cli_tests.rs @@ -4,28 +4,28 @@ //! using `rstest` for parameterised coverage of success and error scenarios. use clap::Parser; use clap::error::ErrorKind; -use netsuke::cli::{Cli, Commands}; +use netsuke::cli::{BuildArgs, Cli, Commands}; use rstest::rstest; use std::path::PathBuf; #[rstest] -#[case(vec!["netsuke"], PathBuf::from("Netsukefile"), None, None, false, Commands::Build { emit: None, targets: Vec::new() })] +#[case(vec!["netsuke"], PathBuf::from("Netsukefile"), None, None, false, Commands::Build(BuildArgs { emit: None, targets: Vec::new() }))] #[case( vec!["netsuke", "--file", "alt.yml", "-C", "work", "-j", "4", "build", "a", "b"], PathBuf::from("alt.yml"), Some(PathBuf::from("work")), Some(4), false, - Commands::Build { emit: None, targets: vec!["a".into(), "b".into()] }, + Commands::Build(BuildArgs { emit: None, targets: vec!["a".into(), "b".into()] }), )] -#[case(vec!["netsuke", "--verbose"], PathBuf::from("Netsukefile"), None, None, true, Commands::Build { emit: None, targets: Vec::new() })] +#[case(vec!["netsuke", "--verbose"], PathBuf::from("Netsukefile"), None, None, true, Commands::Build(BuildArgs { emit: None, targets: Vec::new() }))] #[case( vec!["netsuke", "build", "--emit", "out.ninja", "a"], PathBuf::from("Netsukefile"), None, None, false, - Commands::Build { emit: Some(PathBuf::from("out.ninja")), targets: vec!["a".into()] }, + Commands::Build(BuildArgs { emit: Some(PathBuf::from("out.ninja")), targets: vec!["a".into()] }), )] #[case( vec!["netsuke", "manifest", "out.ninja"], diff --git a/tests/runner_tests.rs b/tests/runner_tests.rs index 623776b9..d0eef025 100644 --- a/tests/runner_tests.rs +++ b/tests/runner_tests.rs @@ -1,4 +1,4 @@ -use netsuke::cli::{Cli, Commands}; +use netsuke::cli::{BuildArgs, Cli, Commands}; use netsuke::runner::{BuildTargets, run, run_ninja}; use rstest::rstest; use serial_test::serial; @@ -16,10 +16,10 @@ fn run_exits_with_manifest_error_on_invalid_version() { directory: None, jobs: None, verbose: false, - command: Some(Commands::Build { + command: Some(Commands::Build(BuildArgs { emit: None, targets: vec![], - }), + })), }; let result = run(&cli); @@ -35,10 +35,10 @@ fn run_ninja_not_found() { directory: None, jobs: None, verbose: false, - command: Some(Commands::Build { + command: Some(Commands::Build(BuildArgs { emit: None, targets: vec![], - }), + })), }; let targets = BuildTargets::new(vec![]); let err = run_ninja( @@ -71,10 +71,10 @@ fn run_executes_ninja_without_persisting_file() { directory: Some(temp.path().to_path_buf()), jobs: None, verbose: false, - command: Some(Commands::Build { + command: Some(Commands::Build(BuildArgs { emit: None, targets: vec![], - }), + })), }; let result = run(&cli); @@ -111,10 +111,10 @@ fn run_build_with_emit_keeps_file() { directory: Some(temp.path().to_path_buf()), jobs: None, verbose: false, - command: Some(Commands::Build { + command: Some(Commands::Build(BuildArgs { emit: Some(emit_path.clone()), targets: vec![], - }), + })), }; let result = run(&cli); diff --git a/tests/steps/cli_steps.rs b/tests/steps/cli_steps.rs index 91544fc7..8ff0dd15 100644 --- a/tests/steps/cli_steps.rs +++ b/tests/steps/cli_steps.rs @@ -6,7 +6,7 @@ use crate::CliWorld; use clap::Parser; use cucumber::{given, then, when}; -use netsuke::cli::{Cli, Commands}; +use netsuke::cli::{BuildArgs, Cli, Commands}; use std::path::PathBuf; fn apply_cli(world: &mut CliWorld, args: &str) { @@ -16,10 +16,10 @@ fn apply_cli(world: &mut CliWorld, args: &str) { match Cli::try_parse_from(tokens) { Ok(mut cli) => { if cli.command.is_none() { - cli.command = Some(Commands::Build { + cli.command = Some(Commands::Build(BuildArgs { emit: None, targets: Vec::new(), - }); + })); } world.cli = Some(cli); world.cli_error = None; @@ -34,7 +34,7 @@ fn apply_cli(world: &mut CliWorld, args: &str) { fn extract_build(world: &CliWorld) -> Option<(&Vec, &Option)> { let cli = world.cli.as_ref()?; match cli.command.as_ref()? { - Commands::Build { targets, emit } => Some((targets, emit)), + Commands::Build(args) => Some((&args.targets, &args.emit)), _ => None, } }