use crate::{AnyResult, Case, CowString, Parse, StdResult, Value, DEFAULT_KEYS_SEPARATOR};
use clap::{
error::Result as ClapResult, parser::ValueSource, value_parser, Arg, ArgAction, ArgMatches,
Command,
};
use serde_yaml::Value as YamlValue;
use std::{
borrow::Cow,
env,
ffi::{OsStr, OsString},
path::PathBuf,
};
pub type Result<T> = StdResult<T, Error>;
pub const DEFAULT_MAX_DEPTH: u8 = 16;
#[non_exhaustive]
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Failed parse as YAML: '{1}'")]
ParseYaml(#[source] serde_yaml::Error, String),
#[error("{1}")]
Clap(#[source] clap::Error, Cow<'static, str>),
#[error("{1}")]
Common(#[source] crate::Error, Cow<'static, str>),
}
pub struct Parser {
value: Value,
}
impl Case for Parser {
#[inline]
fn is_case_sensitive(&self) -> bool {
self.value.is_case_sensitive()
}
}
impl Parse for Parser {
#[inline]
fn parse(&mut self, _value: &Value) -> AnyResult<Value> {
Ok(self.value.clone())
}
}
pub struct ParserBuilder {
command: Command,
args: Option<Vec<OsString>>,
global_key_names: bool,
max_depth: u8,
keys_delimiter: String,
case_sensitive: bool,
exit_on_error: bool,
use_arg_types: bool,
use_defaults: bool,
}
impl ParserBuilder {
#[inline]
pub fn new(command: Command) -> Self {
Self {
command,
args: Default::default(),
global_key_names: true,
max_depth: DEFAULT_MAX_DEPTH,
keys_delimiter: DEFAULT_KEYS_SEPARATOR.to_string(),
case_sensitive: true,
exit_on_error: false,
use_arg_types: true,
use_defaults: false,
}
}
#[inline]
pub fn args<I, T>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = T>,
T: Into<OsString>,
{
self.args = Some(args.into_iter().map(|a| a.into()).collect());
self
}
#[inline]
pub fn global_key_names(&mut self, on: bool) -> &mut Self {
self.global_key_names = on;
self
}
#[inline]
pub fn max_depth(&mut self, depth: u8) -> &mut Self {
self.max_depth = depth;
self
}
#[inline]
pub fn keys_delimiter<S>(&mut self, delim: S) -> &mut Self
where
S: Into<String>,
{
self.keys_delimiter = delim.into();
self
}
#[inline]
pub fn case_sensitive(&mut self, on: bool) -> &mut Self {
self.case_sensitive = on;
self
}
#[inline]
pub fn exit_on_error(&mut self, on: bool) -> &mut Self {
self.exit_on_error = on;
self
}
#[inline]
pub fn use_arg_types(&mut self, on: bool) -> &mut Self {
self.use_arg_types = on;
self
}
#[inline]
pub fn use_defaults(&mut self, on: bool) -> &mut Self {
self.use_defaults = on;
self
}
pub fn build(&mut self) -> Result<Parser> {
let result = self.get_matches();
let matches = if self.exit_on_error {
result.unwrap_or_else(|e| e.exit())
} else {
result.map_err(|e| Error::Clap(e, "Failed to get matches".into()))?
};
let value = self.get_app_arguments(
Value::with_case(self.case_sensitive),
&self.command,
&matches,
"",
self.max_depth,
)?;
Ok(Parser { value })
}
fn get_matches(&mut self) -> ClapResult<ArgMatches> {
if let Some(ref args) = self.args {
self.command.try_get_matches_from_mut(args)
} else {
self.command.try_get_matches_from_mut(env::args_os())
}
}
fn get_app_arguments(
&self,
mut value: Value,
command: &Command,
matches: &ArgMatches,
app_name: &str,
depth: u8,
) -> Result<Value> {
let prefix = if self.global_key_names || app_name.is_empty() {
Default::default()
} else {
[app_name, &self.keys_delimiter].concat()
};
for arg in command.get_arguments().filter(|a| {
let source = matches.value_source(a.get_id().as_str());
source == Some(ValueSource::CommandLine)
|| source == Some(ValueSource::EnvVariable)
|| self.use_defaults && source == Some(ValueSource::DefaultValue)
}) {
value = self.set_value(
value,
matches,
&[&prefix, arg.get_id().as_str()].concat(),
&self.keys_delimiter,
arg,
)?;
}
if depth == 0 {
return Ok(value);
}
for a in command.get_subcommands() {
if let Some(m) = matches.subcommand_matches(a.get_name()) {
value = self.get_app_arguments(
value,
a,
m,
&[&prefix, a.get_name()].concat(),
depth - 1,
)?;
}
}
Ok(value)
}
fn set_value(
&self,
mut value: Value,
matches: &ArgMatches,
path: &str,
delim: &str,
arg: &Arg,
) -> Result<Value> {
if let Some(v) = matches.get_raw(arg.get_id().as_str()) {
let is_string = is_arg_string(arg);
let v: Vec<_> = v
.map(|i| norm_arg_value(i, self.use_arg_types, is_string))
.collect();
let v = match v[..] {
[ref a] if !is_arg_list(arg) => Cow::Borrowed(a.as_ref()),
_ => Cow::Owned(["[", &v.join(","), "]"].concat()),
};
value
.set_by_key_path_with_delim(
path,
delim,
serde_yaml::from_str::<YamlValue>(&v)
.map_err(|e| Error::ParseYaml(e, v.to_string()))?,
)
.map_err(|e| Error::Common(e, format!("Failed to set path: '{path}'").into()))?;
}
Ok(value)
}
}
fn is_arg_list(arg: &Arg) -> bool {
match arg.get_action() {
ArgAction::Append => true,
_ => arg.get_value_delimiter().is_some(),
}
}
fn is_arg_string(arg: &Arg) -> bool {
let type_id = arg.get_value_parser().type_id();
type_id == value_parser!(String).type_id()
|| type_id == value_parser!(OsString).type_id()
|| type_id == value_parser!(PathBuf).type_id()
}
fn norm_arg_value(value: &OsStr, use_type: bool, is_string: bool) -> CowString {
fn quote(c: char) -> bool {
c == '\'' || c == '"'
}
let is_string = use_type && is_string;
if (is_string || !use_type) && value.is_empty() {
return CowString::Borrowed("''");
}
let val = value.to_string_lossy();
if is_string && !val.starts_with(quote) && !val.ends_with(quote) {
return CowString::Owned(["'", &val, "'"].concat());
}
val
}