use clap::{Arg, ArgAction, Command, Parser};
use serde::{Deserialize, Serialize};
use snafu::Snafu;
use crate::{config::Config, ServiceInfo};
const GENERATE_CONFIG_OPT_ID: &str = "generate";
const USE_CONFIG_OPT_ID: &str = "config";
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("Failed to load configuration: {source}"))]
ConfigLoad {
source: crate::Error,
},
#[snafu(display("Failed to parse command-line arguments: {message}"))]
ArgParse {
message: String,
},
#[snafu(display("Failed to generate configuration file: {source}"))]
ConfigGenerateFailed {
source: crate::Error,
},
}
#[derive(clap::Parser, Serialize, Deserialize)]
pub struct NoArguments {}
#[must_use]
pub struct Cli<C, A = NoArguments> {
pub args: A,
pub config: C,
}
impl<'a, C, A> Cli<C, A>
where
A: Parser + Serialize + Deserialize<'a>,
C: Deserialize<'a> + doku::Document,
{
pub fn try_new(
service_info: &ServiceInfo,
env_prefix: impl AsRef<str>,
) -> Result<Option<Self>, Error> {
let arg_command = A::command();
let cmd = Command::new(service_info.name)
.version(service_info.version)
.author(service_info.author)
.about(
arg_command
.get_about()
.map_or_else(|| service_info.description.to_owned(), ToString::to_string),
)
.args(arg_command.get_arguments())
.arg(
Arg::new("config")
.required_unless_present(GENERATE_CONFIG_OPT_ID)
.action(ArgAction::Set)
.long(USE_CONFIG_OPT_ID)
.short('c')
.help("Specifies the toml config file to run the service with"),
)
.arg(
Arg::new(GENERATE_CONFIG_OPT_ID)
.action(ArgAction::Set)
.long(GENERATE_CONFIG_OPT_ID)
.short('g')
.help("Generates a new default toml config file for the service"),
);
let mut arg_matches = cmd.try_get_matches().map_err(|e| Error::ArgParse {
message: e.to_string(),
})?;
if let Some(config_file_path_str) = arg_matches.remove_one::<String>(GENERATE_CONFIG_OPT_ID)
{
crate::config::create_config_file::<C>(config_file_path_str)
.map_err(|source| Error::ConfigGenerateFailed { source })?;
return Ok(None);
}
let Some(config_path_str) = arg_matches.remove_one::<String>(USE_CONFIG_OPT_ID) else {
unreachable!("config is required unless generate is present")
};
let args = A::from_arg_matches_mut(&mut arg_matches).map_err(|e| Error::ArgParse {
message: e.to_string(),
})?;
let env_prefix = env_prefix.as_ref();
let config_result = Config::new(Some(config_path_str), Some(env_prefix));
let config = config_result
.map(|c| c.config)
.map_err(|source| Error::ConfigLoad { source })?;
Ok(Some(Self { args, config }))
}
pub fn new(service_info: &ServiceInfo, env_prefix: impl AsRef<str>) -> Self {
match Self::try_new(service_info, env_prefix) {
Ok(Some(cli)) => cli,
Ok(None) => {
std::process::exit(0);
}
Err(Error::ConfigGenerateFailed { source }) => {
eprintln!("Failed to generate config file: {source}");
std::process::exit(1);
}
Err(Error::ArgParse { message }) => {
eprintln!("{message}");
std::process::exit(1);
}
Err(Error::ConfigLoad { source }) => {
eprintln!("{source}");
std::process::exit(1);
}
}
}
}