Define configuration and CLI contracts (#2)
This commit is contained in:
@@ -11,5 +11,7 @@ name = "ironstorage"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
ironstorage.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
@@ -1,13 +1,173 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::disallowed_types)]
|
||||
|
||||
use clap::Parser;
|
||||
use std::{ffi::OsString, io::Write, process::ExitCode};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about = "A pass-compatible password-store client")]
|
||||
struct Arguments {}
|
||||
use ironstorage::{
|
||||
command::{
|
||||
CliAction, CommandRequest, EXIT_CONFIG, EXIT_SUCCESS, EXIT_UNAVAILABLE, HelpTopic,
|
||||
OtpRequest, help_text, otp_version_text, parse_from, version_text,
|
||||
},
|
||||
config::Config,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
Arguments::parse();
|
||||
println!("{}", ironstorage::PRODUCT_NAME);
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(code) => ExitCode::from(code),
|
||||
Err(()) => ExitCode::from(ironstorage::command::EXIT_FAILURE),
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<u8, ()> {
|
||||
let stdout = std::io::stdout();
|
||||
let stderr = std::io::stderr();
|
||||
run_with(std::env::args_os(), stdout.lock(), stderr.lock())
|
||||
}
|
||||
|
||||
fn run_with<I, T, O, E>(arguments: I, mut stdout: O, mut stderr: E) -> Result<u8, ()>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<OsString> + Clone,
|
||||
O: Write,
|
||||
E: Write,
|
||||
{
|
||||
let action = match parse_from(arguments) {
|
||||
Ok(action) => action,
|
||||
Err(error) => {
|
||||
stderr
|
||||
.write_all(error.to_string().as_bytes())
|
||||
.map_err(|_| ())?;
|
||||
return Ok(error.exit_code());
|
||||
}
|
||||
};
|
||||
match action {
|
||||
CliAction::Display(text) => {
|
||||
stdout.write_all(text.as_bytes()).map_err(|_| ())?;
|
||||
Ok(EXIT_SUCCESS)
|
||||
}
|
||||
CliAction::Run(invocation) => match invocation.request() {
|
||||
CommandRequest::Help { topic } => {
|
||||
stdout
|
||||
.write_all(help_text(*topic).as_bytes())
|
||||
.map_err(|_| ())?;
|
||||
Ok(EXIT_SUCCESS)
|
||||
}
|
||||
CommandRequest::Version => {
|
||||
stdout
|
||||
.write_all(version_text().as_bytes())
|
||||
.map_err(|_| ())?;
|
||||
Ok(EXIT_SUCCESS)
|
||||
}
|
||||
CommandRequest::Otp(OtpRequest::Help) => {
|
||||
stdout
|
||||
.write_all(help_text(Some(HelpTopic::Otp)).as_bytes())
|
||||
.map_err(|_| ())?;
|
||||
Ok(EXIT_SUCCESS)
|
||||
}
|
||||
CommandRequest::Otp(OtpRequest::Version) => {
|
||||
stdout
|
||||
.write_all(otp_version_text().as_bytes())
|
||||
.map_err(|_| ())?;
|
||||
Ok(EXIT_SUCCESS)
|
||||
}
|
||||
_ => match Config::load(invocation.config()) {
|
||||
Ok(_) => {
|
||||
stderr
|
||||
.write_all(
|
||||
b"the command contract is valid, but this storage operation is not available yet\n",
|
||||
)
|
||||
.map_err(|_| ())?;
|
||||
Ok(EXIT_UNAVAILABLE)
|
||||
}
|
||||
Err(error) => {
|
||||
writeln!(stderr, "{error}").map_err(|_| ())?;
|
||||
Ok(EXIT_CONFIG)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{error::Error, ffi::OsString, fs};
|
||||
|
||||
use ironstorage::command::{EXIT_CONFIG, EXIT_SUCCESS, EXIT_UNAVAILABLE, EXIT_USAGE};
|
||||
|
||||
use super::run_with;
|
||||
|
||||
type TestResult = Result<(), Box<dyn Error>>;
|
||||
|
||||
#[test]
|
||||
fn help_and_usage_errors_have_stable_streams_and_exit_codes() -> TestResult {
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let code = run_with(["ironstorage", "--help"], &mut stdout, &mut stderr)
|
||||
.expect("writing to memory cannot fail");
|
||||
assert_eq!(code, EXIT_SUCCESS);
|
||||
assert!(String::from_utf8(stdout)?.contains("Usage"));
|
||||
assert!(stderr.is_empty());
|
||||
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let code = run_with(
|
||||
["ironstorage", "insert", "--echo", "--multiline", "entry"],
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
)
|
||||
.expect("writing to memory cannot fail");
|
||||
assert_eq!(code, EXIT_USAGE);
|
||||
assert!(stdout.is_empty());
|
||||
assert!(String::from_utf8(stderr)?.contains("cannot be used with"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configuration_and_unavailable_operations_have_stable_exit_codes() -> TestResult {
|
||||
let temporary = tempfile::tempdir()?;
|
||||
let missing = temporary.path().join("missing.toml");
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let code = run_with(
|
||||
[
|
||||
OsString::from("ironstorage"),
|
||||
OsString::from("--config"),
|
||||
missing.into_os_string(),
|
||||
OsString::from("show"),
|
||||
OsString::from("entry"),
|
||||
],
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
)
|
||||
.expect("writing to memory cannot fail");
|
||||
assert_eq!(code, EXIT_CONFIG);
|
||||
assert!(stdout.is_empty());
|
||||
assert!(String::from_utf8(stderr)?.contains("configuration file not found"));
|
||||
|
||||
let keys = temporary.path().join("keys");
|
||||
fs::create_dir(&keys)?;
|
||||
let config = temporary.path().join("config.toml");
|
||||
fs::write(
|
||||
&config,
|
||||
"vault = 'vault'\ndefault_key = 'alice'\nkey_material = 'keys'\n",
|
||||
)?;
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let code = run_with(
|
||||
[
|
||||
OsString::from("ironstorage"),
|
||||
OsString::from("--config"),
|
||||
config.into_os_string(),
|
||||
OsString::from("show"),
|
||||
OsString::from("entry"),
|
||||
],
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
)
|
||||
.expect("writing to memory cannot fail");
|
||||
assert_eq!(code, EXIT_UNAVAILABLE);
|
||||
assert!(stdout.is_empty());
|
||||
assert!(String::from_utf8(stderr)?.contains("not available yet"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user