use std::{ffi::OsString, iter, path::PathBuf};
use clap::{ArgMatches, Args, CommandFactory, Error, Parser, Subcommand, error::ErrorKind};
use figment::{Figment, providers::*};
use serde::{Serialize, de::DeserializeOwned};
#[cfg(feature = "derive")]
pub use clap_config_fallback_derive::{ConfigArgs, ConfigParser, ConfigSubcommand};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigFormat {
Toml,
Yaml,
Json,
}
pub trait IntoArgs {
fn into_args(self) -> impl Iterator<Item = String>;
}
pub trait FromArgs: Sized {
fn from_args(args: &ArgMatches) -> Option<Self>;
}
pub trait ConfigSource {
fn config_path(&self) -> Option<&str>;
fn config_format(&self) -> Option<ConfigFormat> {
self.config_path().and_then(|path| {
if cfg!(feature = "toml") && path.ends_with(".toml") {
Some(ConfigFormat::Toml)
} else if cfg!(feature = "yaml") && (path.ends_with(".yaml") || path.ends_with(".yml"))
{
Some(ConfigFormat::Yaml)
} else if cfg!(feature = "json") && path.ends_with(".json") {
Some(ConfigFormat::Json)
} else {
None
}
})
}
}
pub trait ConfigArgs: Sized + Args {
type Opts: Args + Serialize + DeserializeOwned + IntoArgs + FromArgs;
type Config: Serialize + DeserializeOwned;
}
pub trait ConfigSubcommand: Sized + Subcommand {
type Opts: Subcommand + Serialize + DeserializeOwned + IntoArgs + FromArgs;
type Config: Serialize + DeserializeOwned;
}
pub trait ConfigParser: Sized + Parser {
type Opts: Parser + Serialize + DeserializeOwned + IntoArgs + FromArgs + Default + ConfigSource;
type Config: Serialize + DeserializeOwned;
fn parse_with_config() -> Self {
Self::parse_with_config_from(std::env::args_os())
}
fn try_parse_with_config() -> Result<Self, Error> {
Self::try_parse_with_config_from(std::env::args_os())
}
fn parse_with_config_from<I, T>(itr: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
Self::try_parse_with_config_from(itr).unwrap_or_else(|e| e.exit())
}
fn try_parse_with_config_from<I, T>(itr: I) -> Result<Self, Error>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let command = <Self::Opts as CommandFactory>::command();
let command_name = command.get_name().to_owned();
let args = command.try_get_matches_from(itr)?;
let opts = Self::Opts::from_args(&args).unwrap_or_default();
let config = opts.config_path().map(|path| {
let path = PathBuf::from(path);
if !path.exists() {
return Err(Self::command().error(ErrorKind::Io, "configuration file not found"));
}
let config = match opts.config_format() {
#[cfg(feature = "toml")]
Some(ConfigFormat::Toml) => Figment::from(Toml::file(path)),
#[cfg(feature = "yaml")]
Some(ConfigFormat::Yaml) => Figment::from(Yaml::file(path)),
#[cfg(feature = "json")]
Some(ConfigFormat::Json) => Figment::from(Json::file(path)),
_ => {
return Err(Self::command().error(
ErrorKind::InvalidValue,
"unsupported configuration file".to_string(),
));
}
};
config.extract::<Self::Config>().map_err(|e| {
Self::command().error(
ErrorKind::InvalidValue,
format!("invalid configuration file: {e}"),
)
})
});
let mut cli = Figment::from(Serialized::defaults(opts));
if let Some(config) = config {
cli = cli.join(Serialized::defaults(config?));
}
let cli = cli
.extract::<Self::Opts>()
.map_err(|e| Self::command().error(ErrorKind::InvalidValue, e.to_string()))?;
Self::try_parse_from(iter::once(command_name).chain(cli.into_args()))
}
}