Define configuration and CLI contracts (#2)

This commit is contained in:
Hermes Agent
2026-08-09 21:22:31 +00:00
parent 41e56367bd
commit 2dff2d5254
12 changed files with 2927 additions and 12 deletions

View File

@@ -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(())
}
}