use config::{ConfigError, Source, Value};
use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::HashMap;
type Result<T> = std::result::Result<T, ConfigError>;
#[derive(Debug, Clone)]
pub struct CmdLine {
#[allow(dead_code)]
name: String,
contents: Vec<String>,
}
impl Default for CmdLine {
fn default() -> Self {
Self::new()
}
}
impl CmdLine {
pub fn new() -> Self {
CmdLine {
name: "command line".to_string(),
contents: Vec::new(),
}
}
pub fn push_toml_line(&mut self, line: String) {
self.contents.push(line);
}
fn convert_toml_error(
&self,
toml_str: &str,
error_message: &str,
span: &Option<std::ops::Range<usize>>,
) -> String {
let linepos = |idx| toml_str.bytes().take(idx).filter(|b| *b == b'\n').count();
let source_line = span
.as_ref()
.and_then(|range| {
let startline = linepos(range.start);
let endline = linepos(range.end);
(startline == endline).then_some(startline)
})
.and_then(|pos| self.contents.get(pos));
match (source_line, span.as_ref()) {
(Some(source), _) => {
format!("Couldn't parse command line: {error_message} in {source:?}")
}
(None, Some(range)) if toml_str.get(range.clone()).is_some() => format!(
"Couldn't parse command line: {error_message} within {:?}",
&toml_str[range.clone()]
),
_ => format!("Couldn't parse command line: {error_message}"),
}
}
fn build_toml(&self) -> String {
let mut toml_s = String::new();
for line in &self.contents {
toml_s.push_str(tweak_toml_bareword(line).as_ref().unwrap_or(line));
toml_s.push('\n');
}
toml_s
}
}
impl Source for CmdLine {
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
Box::new(self.clone())
}
fn collect(&self) -> Result<HashMap<String, Value>> {
let toml_s = self.build_toml();
let toml_v: toml::Value = match toml::from_str(&toml_s) {
Err(e) => {
return Err(ConfigError::Message(self.convert_toml_error(
&toml_s,
e.message(),
&e.span(),
)))
}
Ok(v) => v,
};
toml_v
.try_into()
.map_err(|e| ConfigError::Foreign(Box::new(e)))
}
}
fn tweak_toml_bareword(s: &str) -> Option<String> {
static RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r#"(?x:
^
[ \t]*
# first capture group: dotted barewords
((?:[a-zA-Z0-9_\-]+\.)*
[a-zA-Z0-9_\-]+)
[ \t]*=[ \t]*
# second group: one bareword without hyphens
([a-zA-Z0-9_]+)
[ \t]*
$)"#,
)
.expect("Built-in regex compilation failed")
});
RE.captures(s).map(|c| format!("{}=\"{}\"", &c[1], &c[2]))
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_duration_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
#[test]
fn bareword_expansion() {
assert_eq!(tweak_toml_bareword("dsfklj"), None);
assert_eq!(tweak_toml_bareword("=99"), None);
assert_eq!(tweak_toml_bareword("=[1,2,3]"), None);
assert_eq!(tweak_toml_bareword("a=b-c"), None);
assert_eq!(tweak_toml_bareword("a=bc"), Some("a=\"bc\"".into()));
assert_eq!(tweak_toml_bareword("a=b_c"), Some("a=\"b_c\"".into()));
assert_eq!(
tweak_toml_bareword("hello.there.now=a_greeting"),
Some("hello.there.now=\"a_greeting\"".into())
);
}
#[test]
fn conv_toml_error() {
let mut cl = CmdLine::new();
cl.push_toml_line("Hello=world".to_string());
cl.push_toml_line("Hola=mundo".to_string());
cl.push_toml_line("Bonjour=monde".to_string());
let toml_s = cl.build_toml();
assert_eq!(
&cl.convert_toml_error(&toml_s, "Nice greeting", &Some(0..13)),
"Couldn't parse command line: Nice greeting in \"Hello=world\""
);
assert_eq!(
&cl.convert_toml_error(&toml_s, "Nice greeting", &Some(99..333)),
"Couldn't parse command line: Nice greeting"
);
assert_eq!(
&cl.convert_toml_error(&toml_s, "Nice greeting with a thing", &Some(0..13)),
"Couldn't parse command line: Nice greeting with a thing in \"Hello=world\""
);
}
#[test]
fn clone_into_box() {
let mut cl = CmdLine::new();
cl.push_toml_line("Molo=Lizwe".to_owned());
let cl2 = cl.clone_into_box();
let v = cl2.collect().unwrap();
assert_eq!(v["Molo"], "Lizwe".into());
}
#[test]
fn parse_good() {
let mut cl = CmdLine::default();
cl.push_toml_line("a=3".to_string());
cl.push_toml_line("bcd=hello".to_string());
cl.push_toml_line("ef=\"gh i\"".to_string());
cl.push_toml_line("w=[1,2,3]".to_string());
let v = cl.collect().unwrap();
assert_eq!(v["a"], "3".into());
assert_eq!(v["bcd"], "hello".into());
assert_eq!(v["ef"], "gh i".into());
assert_eq!(v["w"], vec![1, 2, 3].into());
}
#[test]
fn parse_bad() {
let mut cl = CmdLine::default();
cl.push_toml_line("x=1 1 1 1 1".to_owned());
let v = cl.collect();
assert!(v.is_err());
}
}