mod arg;
pub mod format;
use std::{
ffi::{OsStr, OsString},
fs, iter,
path::{Path, PathBuf},
};
use clap::{CommandFactory, Error, Parser, error::ErrorKind};
use figment::{Figment, providers::*};
use serde::{Serialize, de::DeserializeOwned};
#[cfg(feature = "derive")]
pub use clap_config_fallback_derive::{ConfigArgs, ConfigParser, ConfigSubcommand};
pub use arg::{Arg, FromArgs, IntoArgs};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigFormat {
Toml,
Yaml,
Json,
}
pub trait ConfigSource {
fn config_path(&self) -> Option<&Path>;
fn config_format(&self) -> Option<ConfigFormat> {
self.config_path()
.and_then(|path| match path.extension().and_then(OsStr::to_str) {
#[cfg(feature = "toml")]
Some("toml") => Some(ConfigFormat::Toml),
#[cfg(feature = "yaml")]
Some("yaml" | "yml") => Some(ConfigFormat::Yaml),
#[cfg(feature = "json")]
Some("json") => Some(ConfigFormat::Json),
_ => None,
})
}
}
pub trait ConfigFallback {
type Opts: Serialize + DeserializeOwned + IntoArgs + FromArgs;
type Config: Serialize + DeserializeOwned;
}
pub trait ConfigParser: Sized + Parser + ConfigFallback
where
Self::Opts: Parser + Default + ConfigSource,
{
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>(args: 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().into();
let args = args.into_iter().collect::<Vec<_>>();
for arg in args.iter().cloned() {
if let Some("--help" | "-h" | "--version" | "-V") = arg.into().as_os_str().to_str() {
return Self::try_parse_from(args);
}
}
let args = command.try_get_matches_from(args)?;
let opts = Self::Opts::from_args(&args).unwrap_or_default();
let config: Option<Self::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 content = fs::read_to_string(path).map_err(|e| {
Self::command().error(
ErrorKind::Io,
format!("failed to read configuration file: {e}"),
)
})?;
match opts.config_format() {
#[cfg(feature = "toml")]
Some(ConfigFormat::Toml) => toml::from_str(&content).map_err(|e| e.to_string()),
#[cfg(feature = "yaml")]
Some(ConfigFormat::Yaml) => {
noyalib::from_str_borrowing(&content).map_err(|e| e.to_string())
}
#[cfg(feature = "json")]
Some(ConfigFormat::Json) => {
serde_json::from_str(&content).map_err(|e| e.to_string())
}
_ => Err("unsupported format".to_string()),
}
.map_err(|e| {
Self::command().error(
ErrorKind::InvalidValue,
format!("invalid configuration file: {e}"),
)
})
})
.transpose()?;
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()))
}
}