From e8b76902e1b61aebe11ebac830c9dfa4ea90402d Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 13 Jan 2022 15:51:54 -0600 Subject: [PATCH] refactor(cli)!: Port to clap_derive BREAKING CHANGE: Logging arguments now can't be called on the subcommand and now are repeated `-q` or `-v`, with "info" being the default log level. --- Cargo.lock | 96 +++++++++++++++++ Cargo.toml | 5 +- src/bin/cobalt/args.rs | 147 ++++++++++--------------- src/bin/cobalt/build.rs | 100 ++++++++--------- src/bin/cobalt/debug.rs | 123 ++++++++++----------- src/bin/cobalt/main.rs | 100 +++++++++-------- src/bin/cobalt/new.rs | 230 ++++++++++++++++++---------------------- src/bin/cobalt/serve.rs | 119 ++++++++++----------- tests/cli.rs | 92 +++++++--------- 9 files changed, 515 insertions(+), 497 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 605d020..89524da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -270,6 +270,7 @@ checksum = "d01c9347757e131122b19cd19a05c85805b68c2352a97b623efdc3c295290299" dependencies = [ "atty", "bitflags", + "clap_derive", "indexmap", "lazy_static", "os_str_bytes", @@ -278,6 +279,29 @@ dependencies = [ "textwrap", ] +[[package]] +name = "clap-verbosity-flag" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1281ab1a7abc0f415a57cf6bc1f46282957ce0c5f2b3fe6b98ff3adf8e29b3" +dependencies = [ + "clap", + "log", +] + +[[package]] +name = "clap_derive" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "517358c28fcef6607bf6f76108e02afad7e82297d132a6b846dcc1fc3efcd153" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "cobalt-bin" version = "0.17.5" @@ -286,6 +310,7 @@ dependencies = [ "assert_fs", "chrono", "clap", + "clap-verbosity-flag", "cobalt-config", "cobalt-core", "deunicode", @@ -294,6 +319,7 @@ dependencies = [ "failure", "ghp", "html-minifier", + "human-panic", "ignore", "itertools", "jsonfeed", @@ -308,6 +334,7 @@ dependencies = [ "notify", "open", "predicates", + "proc-exit", "pulldown-cmark", "regex", "relative-path", @@ -872,6 +899,12 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -902,6 +935,21 @@ dependencies = [ "minifier", ] +[[package]] +name = "human-panic" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39f357a500abcbd7c5f967c1d45c8838585b36743823b9d43488f24850534e36" +dependencies = [ + "backtrace", + "os_type", + "serde", + "serde_derive", + "termcolor", + "toml", + "uuid", +] + [[package]] name = "humantime" version = "2.1.0" @@ -1390,6 +1438,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "os_type" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3df761f6470298359f84fcfb60d86db02acc22c251c37265c07a3d1057d2389" +dependencies = [ + "regex", +] + [[package]] name = "pathdiff" version = "0.2.1" @@ -1501,6 +1558,36 @@ dependencies = [ "termtree", ] +[[package]] +name = "proc-exit" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da6bbc8ef87314d4f596ad9d02db375c3f2d77fba91067a6f6a5754fdc8cb49" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -2144,6 +2231,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", +] + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 3d6a786..d02dcac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,10 @@ doc = false [dependencies] cobalt-config = { version = "=0.17.0", path = "crates/config", features = ["unstable"] } cobalt-core = { version = "=0.17.0", path = "crates/core", features = ["unstable"] } -clap = { version = "3.0", features = ["cargo"] } +clap = { version = "3.0", features = ["derive"] } +clap-verbosity-flag = "0.4" +proc-exit = "1" +human-panic = "1" kstring = "1.0" relative-path = { version = "1", features = ["serde"] } liquid = "0.23" diff --git a/src/bin/cobalt/args.rs b/src/bin/cobalt/args.rs index 7e3f1d0..127ebf0 100644 --- a/src/bin/cobalt/args.rs +++ b/src/bin/cobalt/args.rs @@ -6,112 +6,73 @@ use failure::ResultExt; use crate::error::*; -pub fn get_config_args() -> Vec> { - [ - clap::Arg::new("config") - .short('c') - .long("config") - .value_name("FILE") - .help("Config file to use [default: _cobalt.yml]") - .takes_value(true), - clap::Arg::new("destination") - .short('d') - .long("destination") - .value_name("DIR") - .help("Site destination folder [default: ./]") - .takes_value(true), - clap::Arg::new("drafts") - .long("drafts") - .help("Include drafts.") - .takes_value(false), - clap::Arg::new("no-drafts") - .long("no-drafts") - .help("Ignore drafts.") - .conflicts_with("drafts") - .takes_value(false), - ] - .to_vec() +#[derive(Clone, Debug, PartialEq, Eq, clap::Args)] +pub struct ConfigArgs { + /// Config file to use [default: _cobalt.yml] + #[clap(short, long, value_name = "FILE", parse(from_os_str))] + config: Option, + + /// Site destination folder [default: ./] + #[clap(short, long, value_name = "DIR", parse(from_os_str))] + destination: Option, + + /// Include drafts. + #[clap(long)] + drafts: bool, + + /// Ignore drafts. + #[clap(long, conflicts_with = "drafts")] + no_drafts: bool, } -pub fn get_config(matches: &clap::ArgMatches) -> Result { - let config_path = matches.value_of("config"); +impl ConfigArgs { + pub fn load_config(&self) -> Result { + let config_path = self.config.as_deref(); - // Fetch config information if available - let mut config = if let Some(config_path) = config_path { - cobalt_config::Config::from_file(config_path) - .with_context(|_| failure::format_err!("Error reading config file {:?}", config_path))? - } else { - let cwd = env::current_dir().expect("How does this fail?"); - cobalt_config::Config::from_cwd(cwd)? - }; + // Fetch config information if available + let mut config = if let Some(config_path) = config_path { + cobalt_config::Config::from_file(config_path).with_context(|_| { + failure::format_err!("Error reading config file {:?}", config_path) + })? + } else { + let cwd = env::current_dir().expect("How does this fail?"); + cobalt_config::Config::from_cwd(cwd)? + }; - config.abs_dest = matches - .value_of("destination") - .map(|d| { - let d = path::PathBuf::from(d); - std::fs::create_dir_all(&d)?; - d.canonicalize() - }) - .transpose()?; + config.abs_dest = self + .destination + .as_deref() + .map(|d| { + std::fs::create_dir_all(d)?; + d.canonicalize() + }) + .transpose()?; - if matches.is_present("drafts") { - config.include_drafts = true; - } - if matches.is_present("no-drafts") { - config.include_drafts = false; + if let Some(drafts) = self.drafts() { + config.include_drafts = drafts; + } + + Ok(config) } - Ok(config) + pub fn drafts(&self) -> Option { + resolve_bool_arg(self.drafts, self.no_drafts) + } } -pub fn get_logging_args() -> Vec> { - [ - clap::Arg::new("log-level") - .short('L') - .long("log-level") - .possible_values(&["error", "warn", "info", "debug", "trace", "off"]) - .help("Log level [default: info]") - .global(true) - .takes_value(true), - clap::Arg::new("trace") - .long("trace") - .help("Log ultra-verbose (trace level) information") - .global(true) - .takes_value(false), - clap::Arg::new("silent") - .long("silent") - .help("Suppress all output") - .global(true) - .takes_value(false), - ] - .to_vec() +fn resolve_bool_arg(yes: bool, no: bool) -> Option { + match (yes, no) { + (true, false) => Some(true), + (false, true) => Some(false), + (false, false) => None, + (_, _) => unreachable!("clap should make this impossible"), + } } -pub fn get_logging( - global_matches: &clap::ArgMatches, - matches: &clap::ArgMatches, -) -> Result { +pub fn get_logging(level: log::Level) -> Result { let mut builder = env_logger::Builder::new(); - let level = if matches.is_present("trace") { - log::LevelFilter::Trace - } else if matches.is_present("silent") { - log::LevelFilter::Off - } else { - match matches - .value_of("log-level") - .or_else(|| global_matches.value_of("log-level")) - { - Some("error") => log::LevelFilter::Error, - Some("warn") => log::LevelFilter::Warn, - Some("debug") => log::LevelFilter::Debug, - Some("trace") => log::LevelFilter::Trace, - Some("off") => log::LevelFilter::Off, - Some("info") => log::LevelFilter::Info, - _ => log::LevelFilter::Info, - } - }; - builder.filter(None, level); + builder.filter(None, level.to_level_filter()); if level == log::LevelFilter::Trace { builder.format_timestamp_secs(); diff --git a/src/bin/cobalt/build.rs b/src/bin/cobalt/build.rs index 5951865..80fc748 100644 --- a/src/bin/cobalt/build.rs +++ b/src/bin/cobalt/build.rs @@ -5,20 +5,23 @@ use std::path; use crate::args; use crate::error::*; -pub fn build_command_args() -> clap::App<'static> { - clap::App::new("build") - .about("build the cobalt project at the source dir") - .args(args::get_config_args()) +/// Build the cobalt project at the source dir +#[derive(Clone, Debug, PartialEq, Eq, clap::Args)] +pub struct BuildArgs { + #[clap(flatten, help_heading = "CONFIG")] + pub config: args::ConfigArgs, } -pub fn build_command(matches: &clap::ArgMatches) -> Result<()> { - let config = args::get_config(matches)?; - let config = cobalt::cobalt_model::Config::from_config(config)?; +impl BuildArgs { + pub fn run(&self) -> Result<()> { + let config = self.config.load_config()?; + let config = cobalt::cobalt_model::Config::from_config(config)?; - build(config)?; - info!("Build successful"); + build(config)?; + info!("Build successful"); - Ok(()) + Ok(()) + } } pub fn build(config: cobalt::Config) -> Result<()> { @@ -31,17 +34,20 @@ pub fn build(config: cobalt::Config) -> Result<()> { Ok(()) } -pub fn clean_command_args() -> clap::App<'static> { - clap::App::new("clean") - .about("cleans `destination` directory") - .args(args::get_config_args()) +/// Cleans `destination` directory +#[derive(Clone, Debug, PartialEq, Eq, clap::Args)] +pub struct CleanArgs { + #[clap(flatten, help_heading = "CONFIG")] + pub config: args::ConfigArgs, } -pub fn clean_command(matches: &clap::ArgMatches) -> Result<()> { - let config = args::get_config(matches)?; - let config = cobalt::cobalt_model::Config::from_config(config)?; +impl CleanArgs { + pub fn run(&self) -> Result<()> { + let config = self.config.load_config()?; + let config = cobalt::cobalt_model::Config::from_config(config)?; - clean(&config) + clean(&config) + } } pub fn clean(config: &cobalt::Config) -> Result<()> { @@ -70,42 +76,38 @@ pub fn clean(config: &cobalt::Config) -> Result<()> { Ok(()) } -pub fn import_command_args() -> clap::App<'static> { - clap::App::new("import") - .about("moves the contents of the dest folder to the gh-pages branch") - .args(args::get_config_args()) - .arg( - clap::Arg::new("branch") - .short('b') - .long("branch") - .value_name("BRANCH") - .help("Branch that will be used to import the site to") - .default_value("gh-pages") - .takes_value(true), - ) - .arg( - clap::Arg::new("message") - .short('m') - .long("message") - .value_name("COMMIT-MESSAGE") - .help("Commit message that will be used on import") - .default_value("cobalt site import") - .takes_value(true), - ) +/// Moves the contents of the dest folder to the gh-pages branch +#[derive(Clone, Debug, PartialEq, Eq, clap::Args)] +pub struct ImportArgs { + /// Branch that will be used to import the site to + #[clap(short, long, default_value = "gh-pages")] + pub branch: String, + + /// Commit message that will be used on import + #[clap( + short, + long, + value_name = "COMMIT-MESSAGE", + default_value = "cobalt site import" + )] + pub message: String, + + #[clap(flatten, help_heading = "CONFIG")] + pub config: args::ConfigArgs, } -pub fn import_command(matches: &clap::ArgMatches) -> Result<()> { - let config = args::get_config(matches)?; - let config = cobalt::cobalt_model::Config::from_config(config)?; +impl ImportArgs { + pub fn run(&self) -> Result<()> { + let config = self.config.load_config()?; + let config = cobalt::cobalt_model::Config::from_config(config)?; - clean(&config)?; - build(config.clone())?; + clean(&config)?; + build(config.clone())?; - let branch = matches.value_of("branch").unwrap().to_string(); - let message = matches.value_of("message").unwrap().to_string(); - import(&config, &branch, &message)?; + import(&config, &self.branch, &self.message)?; - Ok(()) + Ok(()) + } } fn import(config: &cobalt::Config, branch: &str, message: &str) -> Result<()> { diff --git a/src/bin/cobalt/debug.rs b/src/bin/cobalt/debug.rs index ebab697..337c69b 100644 --- a/src/bin/cobalt/debug.rs +++ b/src/bin/cobalt/debug.rs @@ -1,78 +1,83 @@ use crate::args; use crate::error::*; -pub fn debug_command_args() -> clap::App<'static> { - clap::App::new("debug") - .about("Print site debug information") - .subcommand(clap::App::new("config").about("Prints post-processed config")) - .subcommand( - clap::App::new("highlight") - .about("Print syntax-highlight information") - .subcommand(clap::App::new("themes")) - .subcommand(clap::App::new("syntaxes")), - ) - .subcommand( - clap::App::new("files") - .about("Print files associated with a collection") - .args(args::get_config_args()) - .arg( - clap::Arg::new("COLLECTION") - .help("Collection name") - .index(1), - ), - ) +/// Print site debug information +#[derive(Clone, Debug, PartialEq, Eq, clap::Subcommand)] +pub enum DebugCommands { + /// Prints post-processed config + Config { + #[clap(flatten, help_heading = "CONFIG")] + config: args::ConfigArgs, + }, + + /// Print syntax-highlight information + #[clap(subcommand)] + Highlight(HighlightCommands), + + /// Print files associated with a collection + Files { + /// Collection name + collection: Option, + + #[clap(flatten, help_heading = "CONFIG")] + config: args::ConfigArgs, + }, } -pub fn debug_command(matches: &clap::ArgMatches) -> Result<()> { - match matches.subcommand() { - Some(("config", _)) => { - let config = args::get_config(matches)?; - let config = cobalt::cobalt_model::Config::from_config(config)?; - println!("{}", config); - } - Some(("highlight", matches)) => match matches.subcommand() { - Some(("themes", _)) => { +#[derive(Clone, Debug, PartialEq, Eq, clap::Subcommand)] +pub enum HighlightCommands { + Themes {}, + Syntaxes {}, +} + +impl DebugCommands { + pub fn run(&self) -> Result<()> { + match self { + Self::Config { config } => { + let config = config.load_config()?; + let config = cobalt::cobalt_model::Config::from_config(config)?; + println!("{}", config); + } + Self::Highlight(HighlightCommands::Themes {}) => { for name in cobalt::list_syntax_themes() { println!("{}", name); } } - Some(("syntaxes", _)) => { + Self::Highlight(HighlightCommands::Syntaxes {}) => { for name in cobalt::list_syntaxes() { println!("{}", name); } } - _ => unreachable!("Unexpected subcommand"), - }, - Some(("files", matches)) => { - let config = args::get_config(matches)?; - let config = cobalt::cobalt_model::Config::from_config(config)?; - let collection = matches.value_of("COLLECTION"); - match collection { - Some("assets") => { - failure::bail!("TODO Re-implement"); - } - Some("pages") => { - failure::bail!("TODO Re-implement"); - } - Some("posts") => { - failure::bail!("TODO Re-implement"); - } - None => { - let source_files = cobalt_core::Source::new( - &config.source, - config.ignore.iter().map(|s| s.as_str()), - )?; - for path in source_files.iter() { - println!("{}", path.rel_path); + Self::Files { collection, config } => { + let config = config.load_config()?; + let config = cobalt::cobalt_model::Config::from_config(config)?; + + match collection.as_deref() { + Some("assets") => { + failure::bail!("TODO Re-implement"); + } + Some("pages") => { + failure::bail!("TODO Re-implement"); + } + Some("posts") => { + failure::bail!("TODO Re-implement"); + } + None => { + let source_files = cobalt_core::Source::new( + &config.source, + config.ignore.iter().map(|s| s.as_str()), + )?; + for path in source_files.iter() { + println!("{}", path.rel_path); + } + } + _ => { + failure::bail!("Collection is not yet supported"); } - } - _ => { - failure::bail!("Collection is not yet supported"); } } } - _ => unreachable!("Unexpected subcommand"), - } - Ok(()) + Ok(()) + } } diff --git a/src/bin/cobalt/main.rs b/src/bin/cobalt/main.rs index 0c980ce..7ed7356 100644 --- a/src/bin/cobalt/main.rs +++ b/src/bin/cobalt/main.rs @@ -3,9 +3,6 @@ #[macro_use] extern crate lazy_static; -#[macro_use] -extern crate clap; - #[macro_use] extern crate log; @@ -19,71 +16,72 @@ mod serve; use std::alloc; -use clap::{App, AppSettings}; -use failure::ResultExt; +use clap::{AppSettings, Parser}; use crate::error::*; #[global_allocator] static GLOBAL: alloc::System = alloc::System; -fn main() -> std::result::Result<(), exitfailure::ExitFailure> { - run()?; - Ok(()) +/// Static site generator +#[derive(Clone, Debug, Parser)] +#[clap(global_setting = AppSettings::PropagateVersion)] +#[clap(version)] +struct Cli { + #[clap(flatten)] + pub logging: clap_verbosity_flag::Verbosity, + + #[clap(subcommand)] + command: Command, } -fn cli() -> App<'static> { - let app_cli = App::new("Cobalt") - .version(crate_version!()) - .author("Benny Klotz , Johann Hofmann") - .about("A static site generator written in Rust.") - .setting(AppSettings::SubcommandRequired) - .setting(AppSettings::PropagateVersion) - .args(&args::get_logging_args()) - .subcommand(new::init_command_args()) - .subcommand(new::new_command_args()) - .subcommand(new::rename_command_args()) - .subcommand(new::publish_command_args()) - .subcommand(build::build_command_args()) - .subcommand(build::clean_command_args()) - .subcommand(build::import_command_args()) - .subcommand(debug::debug_command_args()); +#[derive(Clone, Debug, PartialEq, Eq, Parser)] +enum Command { + Init(new::InitArgs), + New(new::NewArgs), + Rename(new::RenameArgs), + Publish(new::PublishArgs), + Build(build::BuildArgs), + Clean(build::CleanArgs), + Import(build::ImportArgs), #[cfg(feature = "serve")] - let app_cli = app_cli.subcommand(serve::serve_command_args()); - app_cli + Serve(serve::ServeArgs), + #[clap(subcommand)] + Debug(debug::DebugCommands), } -fn run() -> Result<()> { - let app_cli = cli(); - let global_matches = app_cli.get_matches(); +impl Cli { + pub fn run(&self) -> Result<()> { + let mut logging = self.logging.clone(); + logging.set_default(Some(log::Level::Info)); + if let Some(level) = logging.log_level() { + let mut builder = args::get_logging(level)?; + builder.init(); + } - let (command, matches) = match global_matches.subcommand() { - Some((command, matches)) => (command, matches), - None => unreachable!(), - }; - - let mut builder = args::get_logging(&global_matches, matches)?; - builder.init(); - - match command { - "init" => new::init_command(matches), - "new" => new::new_command(matches), - "rename" => new::rename_command(matches), - "publish" => new::publish_command(matches), - "build" => build::build_command(matches), - "clean" => build::clean_command(matches), - #[cfg(feature = "serve")] - "serve" => serve::serve_command(matches), - "import" => build::import_command(matches), - "debug" => debug::debug_command(matches), - _ => unreachable!("Unexpected subcommand"), + match &self.command { + Command::Init(cmd) => cmd.run(), + Command::New(cmd) => cmd.run(), + Command::Rename(cmd) => cmd.run(), + Command::Publish(cmd) => cmd.run(), + Command::Build(cmd) => cmd.run(), + Command::Clean(cmd) => cmd.run(), + Command::Import(cmd) => cmd.run(), + #[cfg(feature = "serve")] + Command::Serve(cmd) => cmd.run(), + Command::Debug(cmd) => cmd.run(), + } } - .with_context(|_| failure::format_err!("{} command failed", command))?; +} +fn main() -> std::result::Result<(), exitfailure::ExitFailure> { + let cli = Cli::parse(); + cli.run()?; Ok(()) } #[test] fn verify_app() { - cli().debug_assert() + use clap::IntoApp; + Cli::into_app().debug_assert() } diff --git a/src/bin/cobalt/new.rs b/src/bin/cobalt/new.rs index 56bb982..d90a8cd 100644 --- a/src/bin/cobalt/new.rs +++ b/src/bin/cobalt/new.rs @@ -11,148 +11,130 @@ use failure::ResultExt; use crate::args; use crate::error::*; -pub fn init_command_args() -> clap::App<'static> { - clap::App::new("init") - .about("create a new cobalt project") - .arg( - clap::Arg::new("DIRECTORY") - .help("Target directory") - .default_value("./") - .index(1), - ) +/// Create a document +#[derive(Clone, Debug, PartialEq, Eq, clap::Args)] +pub struct InitArgs { + /// Target directory + #[clap(default_value = "./", parse(from_os_str))] + pub directory: path::PathBuf, } -pub fn init_command(matches: &clap::ArgMatches) -> Result<()> { - let directory = matches.value_of("DIRECTORY").unwrap(); +impl InitArgs { + pub fn run(&self) -> Result<()> { + create_new_project(&self.directory) + .with_context(|_| failure::err_msg("Could not create a new cobalt project"))?; + info!("Created new project at {}", self.directory.display()); - create_new_project(&directory.to_string()) - .with_context(|_| failure::err_msg("Could not create a new cobalt project"))?; - info!("Created new project at {}", directory); - - Ok(()) -} - -pub fn new_command_args() -> clap::App<'static> { - clap::App::new("new") - .about("Create a document") - .args(args::get_config_args()) - .arg( - clap::Arg::new("TITLE") - .required(true) - .help("Title of the post") - .takes_value(true), - ) - .arg( - clap::Arg::new("file") - .short('f') - .long("file") - .value_name("DIR_OR_FILE") - .help("New document's parent directory or file (default: `/title.ext`)") - .takes_value(true), - ) - .arg( - clap::Arg::new("with-ext") - .long("with-ext") - .value_name("EXT") - .help("The default file's extension (e.g. `liquid`)") - .takes_value(true), - ) -} - -pub fn new_command(matches: &clap::ArgMatches) -> Result<()> { - let mut config = args::get_config(matches)?; - config.include_drafts = true; - let config = cobalt::cobalt_model::Config::from_config(config)?; - - let title = matches.value_of("TITLE").unwrap(); - - let mut file = env::current_dir().expect("How does this fail?"); - if let Some(rel_file) = matches.value_of("file") { - file.push(path::Path::new(rel_file)) + Ok(()) } - - let ext = matches.value_of("with-ext"); - - create_new_document(&config, title, file, ext) - .with_context(|_| failure::format_err!("Could not create `{}`", title))?; - - Ok(()) } -pub fn rename_command_args() -> clap::App<'static> { - clap::App::new("rename") - .about("Rename a document") - .args(args::get_config_args()) - .arg( - clap::Arg::new("SRC") - .required(true) - .help("File to rename") - .takes_value(true), - ) - .arg( - clap::Arg::new("TITLE") - .required(true) - .help("Title of the post") - .takes_value(true), - ) - .arg( - clap::Arg::new("file") - .short('f') - .long("file") - .value_name("DIR_OR_FILE") - .help("New document's parent directory or file (default: `/title.ext`)") - .takes_value(true), - ) +/// Create a document +#[derive(Clone, Debug, PartialEq, Eq, clap::Args)] +pub struct NewArgs { + /// Title of the post + pub title: String, + + /// New document's parent directory or file (default: `/title.ext`) + #[clap(short, long, value_name = "DIR_OR_FILE", parse(from_os_str))] + pub file: Option, + + /// The default file's extension (e.g. `liquid`) + #[clap(long, value_name = "EXT")] + pub with_ext: Option, + + #[clap(flatten, help_heading = "CONFIG")] + pub config: args::ConfigArgs, } -pub fn rename_command(matches: &clap::ArgMatches) -> Result<()> { - let mut config = args::get_config(matches)?; - config.include_drafts = true; - let config = cobalt::cobalt_model::Config::from_config(config)?; +impl NewArgs { + pub fn run(&self) -> Result<()> { + let mut config = self.config.load_config()?; + config.include_drafts = true; + let config = cobalt::cobalt_model::Config::from_config(config)?; - let source = path::PathBuf::from(matches.value_of("SRC").unwrap()); + let title = self.title.as_ref(); - let title = matches.value_of("TITLE").unwrap(); + let mut file = env::current_dir().expect("How does this fail?"); + if let Some(rel_file) = self.file.as_deref() { + file.push(rel_file) + } - let mut file = env::current_dir().expect("How does this fail?"); - if let Some(rel_file) = matches.value_of("file") { - file.push(path::Path::new(rel_file)) + let ext = self.with_ext.as_deref(); + + create_new_document(&config, title, file, ext) + .with_context(|_| failure::format_err!("Could not create `{}`", title))?; + + Ok(()) } - let file = file; - - rename_document(&config, source, title, file) - .with_context(|_| failure::format_err!("Could not rename `{}`", title))?; - - Ok(()) } -pub fn publish_command_args() -> clap::App<'static> { - clap::App::new("publish") - .about("Publish a document") - .args(args::get_config_args()) - .arg( - clap::Arg::new("FILENAME") - .required(true) - .help("Document path to publish") - .takes_value(true), - ) +/// Rename a document +#[derive(Clone, Debug, PartialEq, Eq, clap::Args)] +pub struct RenameArgs { + /// File to rename + #[clap(value_name = "FILE", parse(from_os_str))] + pub src: path::PathBuf, + + /// Title of the post + pub title: String, + + /// New document's parent directory or file (default: `/title.ext`) + #[clap(short, long, value_name = "DIR_OR_FILE", parse(from_os_str))] + pub file: Option, + + #[clap(flatten, help_heading = "CONFIG")] + pub config: args::ConfigArgs, } -pub fn publish_command(matches: &clap::ArgMatches) -> Result<()> { - let filename = matches - .value_of("FILENAME") - .expect("required parameters are present"); - let mut file = env::current_dir().expect("How does this fail?"); - file.push(path::Path::new(filename)); - let file = file; - let mut config = args::get_config(matches)?; - config.include_drafts = true; - let config = cobalt::cobalt_model::Config::from_config(config)?; +impl RenameArgs { + pub fn run(&self) -> Result<()> { + let mut config = self.config.load_config()?; + config.include_drafts = true; + let config = cobalt::cobalt_model::Config::from_config(config)?; - publish_document(&config, &file) - .with_context(|_| failure::format_err!("Could not publish `{:?}`", file))?; + let source = self.src.clone(); - Ok(()) + let title = self.title.as_ref(); + + let mut file = env::current_dir().expect("How does this fail?"); + if let Some(rel_file) = self.file.as_deref() { + file.push(rel_file) + } + + rename_document(&config, source, title, file) + .with_context(|_| failure::format_err!("Could not rename `{}`", title))?; + + Ok(()) + } +} + +/// Publish a document +#[derive(Clone, Debug, PartialEq, Eq, clap::Args)] +pub struct PublishArgs { + /// Document to publish + #[clap(value_name = "FILE", parse(from_os_str))] + pub filename: path::PathBuf, + + #[clap(flatten, help_heading = "CONFIG")] + pub config: args::ConfigArgs, +} + +impl PublishArgs { + pub fn run(&self) -> Result<()> { + let mut config = self.config.load_config()?; + config.include_drafts = true; + let config = cobalt::cobalt_model::Config::from_config(config)?; + + let filename = self.filename.as_path(); + let mut file = env::current_dir().expect("How does this fail?"); + file.push(path::Path::new(filename)); + + publish_document(&config, &file) + .with_context(|_| failure::format_err!("Could not publish `{:?}`", file))?; + + Ok(()) + } } const COBALT_YML: &str = " diff --git a/src/bin/cobalt/serve.rs b/src/bin/cobalt/serve.rs index 726a781..7e855ba 100644 --- a/src/bin/cobalt/serve.rs +++ b/src/bin/cobalt/serve.rs @@ -15,76 +15,67 @@ use crate::args; use crate::build; use crate::error::*; -pub fn serve_command_args() -> clap::App<'static> { - clap::App::new("serve") - .about("build, serve, and watch the project at the source dir") - .args(args::get_config_args()) - .arg( - clap::Arg::new("port") - .short('P') - .long("port") - .value_name("INT") - .help("Port to serve from") - .default_value("3000") - .takes_value(true), - ) - .arg( - clap::Arg::new("host") - .long("host") - .value_name("host-name/IP") - .help("Host to serve from") - .default_value("localhost") - .takes_value(true), - ) - .arg( - clap::Arg::new("no-watch") - .long("no-watch") - .help("Disable rebuilding on change") - .conflicts_with("drafts") - .takes_value(false), - ) - .arg( - clap::Arg::new("open") - .long("open") - .help("Open in browser") - .takes_value(false), - ) +/// Build, serve, and watch the project at the source dir +#[derive(Clone, Debug, PartialEq, Eq, clap::Args)] +pub struct ServeArgs { + /// Open a browser + #[clap(long)] + pub open: bool, + + /// Host to serve from + #[clap(long, value_name = "HOSTNAME_OR_IP", default_value = "localhost")] + pub host: String, + + /// Port to serve from + #[clap(short = 'P', long, value_name = "NUM", default_value_t = 3000)] + pub port: usize, + + /// Disable rebuilding on change + #[clap(long)] + pub no_watch: bool, + + #[clap(flatten, help_heading = "CONFIG")] + pub config: args::ConfigArgs, } -pub fn serve_command(matches: &clap::ArgMatches) -> Result<()> { - let host = matches.value_of("host").unwrap().to_string(); - let port = matches.value_of("port").unwrap().to_string(); - let ip = format!("{}:{}", host, port); - let open_in_browser = matches.is_present("open"); - let url = format!("http://{}", ip); +impl ServeArgs { + pub fn run(&self) -> Result<()> { + let host = self.host.as_str(); + let port = self.port; + let ip = format!("{}:{}", host, port); + let url = format!("http://{}", ip); + let open_in_browser = self.open; - let mut config = args::get_config(matches)?; - debug!("Overriding config `site.base_url` with `{}`", ip); - config.site.base_url = Some(format!("http://{}", ip).into()); - let config = cobalt::cobalt_model::Config::from_config(config)?; - let dest = path::Path::new(&config.destination).to_owned(); + let mut config = self.config.load_config()?; + debug!("Overriding config `site.base_url` with `{}`", ip); + config.site.base_url = Some(format!("http://{}", ip).into()); + let config = cobalt::cobalt_model::Config::from_config(config)?; - build::build(config.clone())?; + let dest = path::Path::new(&config.destination).to_owned(); - if open_in_browser { - open_browser(url)?; + build::build(config.clone())?; + + if open_in_browser { + open_browser(url)?; + } + + if self.no_watch { + serve(&dest, &ip)?; + } else { + info!("Watching {:?} for changes", &config.source); + thread::spawn(move || { + let e = serve(&dest, &ip); + if let Some(e) = e.err() { + error!("{}", e); + } + process::exit(1) + }); + + watch(&config)?; + } + + Ok(()) } - - if matches.is_present("no-watch") { - serve(&dest, &ip)?; - } else { - info!("Watching {:?} for changes", &config.source); - thread::spawn(move || { - let e = serve(&dest, &ip); - if let Some(e) = e.err() { - error!("{}", e); - } - process::exit(1) - }); - - watch(&config)?; - } - Ok(()) } fn static_file_handler(dest: &path::Path, req: Request) -> Result<()> { diff --git a/tests/cli.rs b/tests/cli.rs index 8bba806..7bd7af3 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -10,7 +10,7 @@ pub fn invalid_calls() { .unwrap() .assert() .failure() - .stderr(predicate::str::contains("requires a subcommand").from_utf8()); + .stderr(predicate::str::contains("SUBCOMMANDS:").from_utf8()); process::Command::cargo_bin("cobalt") .unwrap() @@ -29,27 +29,7 @@ pub fn log_levels_trace() { process::Command::cargo_bin("cobalt") .unwrap() - .args(&["build", "-L", "trace"]) - .current_dir(project_root.path()) - .assert() - .success() - .stderr(predicate::str::contains("TRACE").from_utf8()) - .stderr(predicate::str::contains("DEBUG").from_utf8()) - .stderr(predicate::str::contains("INFO").from_utf8()); - - project_root.close().unwrap(); -} - -#[test] -pub fn log_levels_trace_alias() { - let project_root = assert_fs::TempDir::new().unwrap(); - project_root - .copy_from("tests/fixtures/example", &["**"]) - .unwrap(); - - process::Command::cargo_bin("cobalt") - .unwrap() - .args(&["build", "--trace"]) + .args(&["-vv", "build"]) .current_dir(project_root.path()) .assert() .success() @@ -69,7 +49,7 @@ pub fn log_levels_debug() { process::Command::cargo_bin("cobalt") .unwrap() - .args(&["build", "-L", "debug"]) + .args(&["-v", "build"]) .current_dir(project_root.path()) .assert() .success() @@ -89,7 +69,7 @@ pub fn log_levels_info() { process::Command::cargo_bin("cobalt") .unwrap() - .args(&["build", "-L", "info"]) + .args(&["build"]) .current_dir(project_root.path()) .assert() .success() @@ -109,7 +89,7 @@ pub fn log_levels_silent() { process::Command::cargo_bin("cobalt") .unwrap() - .args(&["build", "--silent"]) + .args(&["-qqqq", "build"]) .current_dir(project_root.path()) .assert() .success() @@ -130,7 +110,7 @@ pub fn clean() { process::Command::cargo_bin("cobalt") .unwrap() - .args(&["build", "--trace", "-d", "_dest"]) + .args(&["-vv", "build", "-d", "_dest"]) .current_dir(project_root.path()) .assert() .success(); @@ -138,7 +118,7 @@ pub fn clean() { process::Command::cargo_bin("cobalt") .unwrap() - .args(&["clean", "--trace", "-d", "_dest"]) + .args(&["-vv", "clean", "-d", "_dest"]) .current_dir(project_root.path()) .assert() .success(); @@ -158,7 +138,7 @@ pub fn clean_empty() { process::Command::cargo_bin("cobalt") .unwrap() - .args(&["clean", "--trace", "-d", "_dest"]) + .args(&["-vv", "clean", "-d", "_dest"]) .current_dir(project_root.path()) .assert() .success(); @@ -175,7 +155,7 @@ pub fn init_project_can_build() { process::Command::cargo_bin("cobalt") .unwrap() - .args(&["init", "--trace"]) + .args(&["-vv", "init"]) .current_dir(project_root.path()) .assert() .success(); @@ -183,7 +163,7 @@ pub fn init_project_can_build() { process::Command::cargo_bin("cobalt") .unwrap() - .args(&["build", "--trace", "-d", "_dest", "--drafts"]) + .args(&["-vv", "build", "-d", "_dest", "--drafts"]) .current_dir(project_root.path()) .assert() .success(); @@ -200,14 +180,14 @@ pub fn new_page_can_build() { process::Command::cargo_bin("cobalt") .unwrap() - .args(&["init", "--trace"]) + .args(&["-vv", "init"]) .current_dir(project_root.path()) .assert() .success(); process::Command::cargo_bin("cobalt") .unwrap() - .args(&["new", "--trace", "My New Special Page"]) + .args(&["-vv", "new", "My New Special Page"]) .current_dir(project_root.path()) .assert() .success(); @@ -218,7 +198,7 @@ pub fn new_page_can_build() { dest.assert(predicate::path::missing()); process::Command::cargo_bin("cobalt") .unwrap() - .args(&["build", "--trace", "-d", "_dest", "--drafts"]) + .args(&["-vv", "build", "-d", "_dest", "--drafts"]) .current_dir(project_root.path()) .assert() .success(); @@ -235,14 +215,14 @@ pub fn new_post_can_build() { process::Command::cargo_bin("cobalt") .unwrap() - .args(&["init", "--trace"]) + .args(&["-vv", "init"]) .current_dir(project_root.path()) .assert() .success(); process::Command::cargo_bin("cobalt") .unwrap() - .args(&["new", "--trace", "My New Special Post"]) + .args(&["-vv", "new", "My New Special Post"]) .current_dir(project_root.path().join("posts")) .assert() .success(); @@ -253,7 +233,7 @@ pub fn new_post_can_build() { dest.assert(predicate::path::missing()); process::Command::cargo_bin("cobalt") .unwrap() - .args(&["build", "--trace", "-d", "_dest", "--drafts"]) + .args(&["-vv", "build", "-d", "_dest", "--drafts"]) .current_dir(project_root.path()) .assert() .success(); @@ -270,14 +250,14 @@ pub fn rename_page_can_build() { process::Command::cargo_bin("cobalt") .unwrap() - .args(&["init", "--trace"]) + .args(&["-vv", "init"]) .current_dir(project_root.path()) .assert() .success(); process::Command::cargo_bin("cobalt") .unwrap() - .args(&["new", "--trace", "My New Special Page"]) + .args(&["-vv", "new", "My New Special Page"]) .current_dir(project_root.path()) .assert() .success(); @@ -288,8 +268,8 @@ pub fn rename_page_can_build() { process::Command::cargo_bin("cobalt") .unwrap() .args(&[ + "-vv", "rename", - "--trace", "my-new-special-page.md", "New and Improved!", ]) @@ -306,7 +286,7 @@ pub fn rename_page_can_build() { dest.assert(predicate::path::missing()); process::Command::cargo_bin("cobalt") .unwrap() - .args(&["build", "--trace", "-d", "_dest", "--drafts"]) + .args(&["-vv", "build", "-d", "_dest", "--drafts"]) .current_dir(project_root.path()) .assert() .success(); @@ -323,14 +303,14 @@ pub fn rename_post_can_build() { process::Command::cargo_bin("cobalt") .unwrap() - .args(&["init", "--trace"]) + .args(&["-vv", "init"]) .current_dir(project_root.path()) .assert() .success(); process::Command::cargo_bin("cobalt") .unwrap() - .args(&["new", "--trace", "My New Special Post"]) + .args(&["-vv", "new", "My New Special Post"]) .current_dir(project_root.path().join("posts")) .assert() .success(); @@ -341,8 +321,8 @@ pub fn rename_post_can_build() { process::Command::cargo_bin("cobalt") .unwrap() .args(&[ + "-vv", "rename", - "--trace", "my-new-special-post.md", "New and Improved!", ]) @@ -359,7 +339,7 @@ pub fn rename_post_can_build() { dest.assert(predicate::path::missing()); process::Command::cargo_bin("cobalt") .unwrap() - .args(&["build", "--trace", "-d", "_dest", "--drafts"]) + .args(&["-vv", "build", "-d", "_dest", "--drafts"]) .current_dir(project_root.path()) .assert() .success(); @@ -376,7 +356,7 @@ pub fn publish_post_can_build() { process::Command::cargo_bin("cobalt") .unwrap() - .args(&["init", "--trace"]) + .args(&["-vv", "init"]) .current_dir(project_root.path()) .assert() .success(); @@ -397,7 +377,7 @@ posts: process::Command::cargo_bin("cobalt") .unwrap() - .args(&["new", "--trace", "My New Special Post"]) + .args(&["-vv", "new", "My New Special Post"]) .current_dir(project_root.path().join("posts")) .assert() .success(); @@ -407,7 +387,7 @@ posts: process::Command::cargo_bin("cobalt") .unwrap() - .args(&["publish", "--trace", "my-new-special-post.md"]) + .args(&["-vv", "publish", "my-new-special-post.md"]) .current_dir(project_root.path().join("posts")) .assert() .success(); @@ -418,7 +398,7 @@ posts: dest.assert(predicate::path::missing()); process::Command::cargo_bin("cobalt") .unwrap() - .args(&["build", "--trace", "-d", "_dest", "--drafts"]) + .args(&["-vv", "build", "-d", "_dest", "--drafts"]) .current_dir(project_root.path()) .assert() .success(); @@ -435,7 +415,7 @@ pub fn publish_date_in_post() { process::Command::cargo_bin("cobalt") .unwrap() - .args(&["init", "--trace"]) + .args(&["-vv", "init"]) .current_dir(project_root.path()) .assert() .success(); @@ -456,7 +436,7 @@ posts: process::Command::cargo_bin("cobalt") .unwrap() - .args(&["new", "--trace", "My New Special Post"]) + .args(&["-vv", "new", "My New Special Post"]) .current_dir(project_root.path().join("posts")) .assert() .success(); @@ -466,7 +446,7 @@ posts: process::Command::cargo_bin("cobalt") .unwrap() - .args(&["publish", "--trace", "my-new-special-post.md"]) + .args(&["-vv", "publish", "my-new-special-post.md"]) .current_dir(project_root.path().join("posts")) .assert() .success(); @@ -477,7 +457,7 @@ posts: dest.assert(predicate::path::missing()); process::Command::cargo_bin("cobalt") .unwrap() - .args(&["build", "--trace", "-d", "_dest", "--drafts"]) + .args(&["-vv", "build", "-d", "_dest", "--drafts"]) .current_dir(project_root.path()) .assert() .success(); @@ -494,7 +474,7 @@ pub fn publish_draft_moves_dir() { process::Command::cargo_bin("cobalt") .unwrap() - .args(&["init", "--trace"]) + .args(&["-vv", "init"]) .current_dir(project_root.path()) .assert() .success(); @@ -517,7 +497,7 @@ posts: process::Command::cargo_bin("cobalt") .unwrap() - .args(&["new", "--trace", "My New Special Post"]) + .args(&["-vv", "new", "My New Special Post"]) .current_dir(project_root.path().join("_drafts")) .assert() .success(); @@ -527,7 +507,7 @@ posts: process::Command::cargo_bin("cobalt") .unwrap() - .args(&["publish", "--trace", "my-new-special-post.md"]) + .args(&["-vv", "publish", "my-new-special-post.md"]) .current_dir(project_root.path().join("_drafts")) .assert() .success(); @@ -541,7 +521,7 @@ posts: dest.assert(predicate::path::missing()); process::Command::cargo_bin("cobalt") .unwrap() - .args(&["build", "--trace", "-d", "_dest", "--drafts"]) + .args(&["-vv", "build", "-d", "_dest", "--drafts"]) .current_dir(project_root.path()) .assert() .success();