diff --git a/Cargo.lock b/Cargo.lock index 5f1369c..2bc027e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1674,6 +1674,7 @@ dependencies = [ "card-backend-pcsc", "clap", "clap_complete", + "keyfork-bin", "keyfork-derive-openpgp", "keyfork-derive-util", "keyfork-entropy", @@ -1692,6 +1693,13 @@ dependencies = [ "tokio", ] +[[package]] +name = "keyfork-bin" +version = "0.1.0" +dependencies = [ + "anyhow", +] + [[package]] name = "keyfork-crossterm" version = "0.27.1" diff --git a/Cargo.toml b/Cargo.toml index 3e3e872..2f9e2cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "crates/qrcode/keyfork-qrcode", "crates/qrcode/keyfork-zbar", "crates/qrcode/keyfork-zbar-sys", + "crates/util/keyfork-bin", "crates/util/keyfork-crossterm", "crates/util/keyfork-entropy", "crates/util/keyfork-frame", diff --git a/crates/keyfork/Cargo.toml b/crates/keyfork/Cargo.toml index 133b79c..c62f082 100644 --- a/crates/keyfork/Cargo.toml +++ b/crates/keyfork/Cargo.toml @@ -43,3 +43,4 @@ openpgp-card-sequoia = { version = "0.2.0", default-features = false } openpgp-card = "0.4.1" clap_complete = { version = "4.4.6", optional = true } sequoia-openpgp = { version = "1.17.0", default-features = false, features = ["compression"] } +keyfork-bin = { version = "0.1.0", path = "../util/keyfork-bin" } diff --git a/crates/keyfork/src/main.rs b/crates/keyfork/src/main.rs index 58ec548..24a7bce 100644 --- a/crates/keyfork/src/main.rs +++ b/crates/keyfork/src/main.rs @@ -6,21 +6,16 @@ use std::process::ExitCode; use clap::Parser; +use keyfork_bin::{Bin, ClosureBin}; + mod cli; mod config; fn main() -> ExitCode { - let opts = cli::Keyfork::parse(); + let bin = ClosureBin::new(|| { + let opts = cli::Keyfork::parse(); + opts.command.handle(&opts) + }); - if let Err(e) = opts.command.handle(&opts) { - eprintln!("Unable to run command: {e}"); - let mut source = e.source(); - while let Some(new_error) = source.take() { - eprintln!("Source: {new_error}"); - source = new_error.source(); - } - return ExitCode::FAILURE; - } - - ExitCode::SUCCESS + bin.main() } diff --git a/crates/util/keyfork-bin/Cargo.toml b/crates/util/keyfork-bin/Cargo.toml new file mode 100644 index 0000000..b1c9c5a --- /dev/null +++ b/crates/util/keyfork-bin/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "keyfork-bin" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +[dev-dependencies] +anyhow = "1.0.79" diff --git a/crates/util/keyfork-bin/src/lib.rs b/crates/util/keyfork-bin/src/lib.rs new file mode 100644 index 0000000..53cce50 --- /dev/null +++ b/crates/util/keyfork-bin/src/lib.rs @@ -0,0 +1,140 @@ +#![allow(clippy::needless_doctest_main)] + +//! A convenient trait for quickly writing binaries in a consistent pattern. +//! +//! # Examples +//! ```rust +//! use anyhow::anyhow; +//! use keyfork_bin::Bin; +//! +//! struct Main; +//! +//! impl Bin for Main { +//! type Args = (String, String); +//! +//! fn usage_hint(&self) -> Option { +//! Some(String::from(" ")) +//! } +//! +//! fn validate_args(&self, mut args: impl Iterator) -> keyfork_bin::ProcessResult { +//! let arg1 = args.next().ok_or(anyhow!("missing argument 1"))?; +//! let arg2 = args.next().ok_or(anyhow!("missing argument 2"))?; +//! Ok((arg1, arg2)) +//! } +//! +//! fn run(&self, (arg1, arg2): Self::Args) -> keyfork_bin::ProcessResult { +//! println!("First argument: {arg1}"); +//! println!("Second argument: {arg2}"); +//! Ok(()) +//! } +//!# +//!# fn main(&self) -> std::process::ExitCode { +//!# self.main_inner([String::from("hello"), String::from("world")].into_iter()) +//!# } +//! } +//! +//! fn main() { +//! // Assume the program was called with something like "hello world"... +//! let bin = Main; +//! bin.main(); +//! } +//! ``` + +use std::process::ExitCode; + +/// A result that may contain any error. +pub type ProcessResult = Result>; + +fn report_err(e: Box) { + eprintln!("Unable to run command: {e}"); + let mut source = e.source(); + while let Some(new_error) = source.take() { + eprintln!("- Caused by: {new_error}"); + source = new_error.source(); + } +} + +/// A trait for implementing the flow of a binary's execution. +pub trait Bin { + /// The type for command-line arguments required by the function. + type Args; + + /// A usage hint for how the arguments should be provided to the program. + fn usage_hint(&self) -> Option { + None + } + + /// Validate the arguments provided by the user into types required by the binary. + #[allow(clippy::missing_errors_doc)] + fn validate_args(&self, args: impl Iterator) -> ProcessResult; + + /// Run the binary + #[allow(clippy::missing_errors_doc)] + fn run(&self, args: Self::Args) -> ProcessResult; + + /// The default handler for running the binary and reporting any errors. + fn main(&self) -> ExitCode { + self.main_inner(std::env::args()) + } + + #[doc(hidden)] + fn main_inner(&self, mut args: impl Iterator) -> ExitCode { + let command = args.next(); + let args = match self.validate_args(args) { + Ok(args) => args, + Err(e) => { + if let (Some(command), Some(hint)) = (command, self.usage_hint()) { + eprintln!("Usage: {command} {hint}"); + } + report_err(e); + return ExitCode::FAILURE; + } + }; + + if let Err(e) = self.run(args) { + report_err(e); + return ExitCode::FAILURE; + } + + ExitCode::SUCCESS + } +} + +/// A Bin that doesn't take any arguments. +pub struct ClosureBin ProcessResult> { + closure: F +} + +impl ClosureBin where F: Fn() -> ProcessResult { + /// Create a new Bin from a closure. + /// + /// # Examples + /// ```rust + /// use keyfork_bin::{Bin, ClosureBin}; + /// + /// let bin = ClosureBin::new(|| { + /// println!("Hello, world!"); + /// Ok(()) + /// }); + /// + /// bin.main(); + /// ``` + pub fn new(closure: F) -> Self { + Self { + closure + } + } +} + +impl Bin for ClosureBin where F: Fn() -> ProcessResult { + type Args = (); + + fn validate_args(&self, _args: impl Iterator) -> ProcessResult { + Ok(()) + } + + fn run(&self, _args: Self::Args) -> ProcessResult { + let c = &self.closure; + c() + } +}