use std::fmt;
use std::env;
use std::error::Error;
use crate::error::ConfigError;
use crate::item::{StringItem, MapAction};
use std::iter::FromIterator;
#[derive(Debug)]
pub enum ProcessingError {
MissingQuotes
}
impl fmt::Display for ProcessingError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::MissingQuotes => write!(f, "value must be quoted.")
}
}
}
impl Error for ProcessingError {
}
pub trait Explode where Self: Sized {
fn explode(self, delimiter: char) -> Result<StringItem, ConfigError>;
}
impl Explode for Result<StringItem, ConfigError> {
fn explode(self, delimiter: char) -> Result<StringItem, ConfigError> {
self?.map(|v| {
MapAction::Replace(Vec::from_iter(v.split(delimiter).map(|v| {
String::from(v)
})))
})
}
}
pub trait Trim where Self: Sized {
fn trim(self) -> Result<StringItem, ConfigError>;
fn trim_start(self) -> Result<StringItem, ConfigError>;
fn trim_end(self) -> Result<StringItem, ConfigError>;
}
impl Trim for Result<StringItem, ConfigError> {
fn trim(self) -> Result<StringItem, ConfigError> {
self?.map(|v| {
if v.starts_with(char::is_whitespace) || v.ends_with(char::is_whitespace){
MapAction::Replace(vec!(String::from(v.trim())))
} else {
MapAction::Keep
}
})
}
fn trim_start(self) -> Result<StringItem, ConfigError> {
self?.map(|v| {
if v.starts_with(char::is_whitespace) {
MapAction::Replace(vec!(String::from(v.trim_start())))
} else {
MapAction::Keep
}
})
}
fn trim_end(self) -> Result<StringItem, ConfigError> {
self?.map(|v| {
if v.ends_with(char::is_whitespace) {
MapAction::Replace(vec!(String::from(v.trim_end())))
} else {
MapAction::Keep
}
})
}
}
pub trait Unescape where Self: Sized {
fn unescape(self) -> Result<StringItem, ConfigError>;
}
impl Unescape for Result<StringItem, ConfigError> {
fn unescape(self) -> Result<StringItem, ConfigError> {
self?.map(|v| {
let mut output = String::with_capacity(v.len() + 10);
let mut chars = v.chars();
while let Some(c) = chars.next() {
output.push(match c {
'\\' => match chars.next() {
Some('n') => '\n',
Some('r') => '\r',
Some('t') => '\t',
Some(x) => x,
None => '\\'
}
x => x
});
}
MapAction::Replace(vec!(output))
})
}
}
pub trait NotEmpty where Self: Sized {
fn not_empty(self) -> Result<StringItem, ConfigError>;
}
impl NotEmpty for Result<StringItem, ConfigError> {
fn not_empty(self) -> Result<StringItem, ConfigError> {
self?.map(|v| {
if v.trim().is_empty() {
MapAction::Drop
} else {
MapAction::Keep
}
})
}
}
pub trait Unquote where Self: Sized {
fn unquote(self) -> Result<StringItem, ConfigError>;
}
impl Unquote for Result<StringItem, ConfigError> {
fn unquote(self) -> Result<StringItem, ConfigError> {
self?.map(|v| {
let v = v.trim();
if v.starts_with('"') && v.ends_with('"') {
MapAction::Replace(vec!(v[1..v.len()-1].to_owned()))
} else {
MapAction::Fail(Box::new(ProcessingError::MissingQuotes))
}
})
}
}
fn expand(input: &str, enabler: char, start: char, end: char, resolver: &dyn Fn(&str) -> Result<String, Box<dyn Error>>) -> Result<String, Box<dyn Error>> {
enum EnvState { Text, ProtoPlaceholder((usize, usize)), InPlaceholder((usize, usize)), Escaped }
let mut result = String::with_capacity(input.len());
let mut state = EnvState::Text;
for (pos, c) in input.char_indices() {
if let Some(next_state) = match &state
{
EnvState::Text if c == enabler => {
let len_to_start = result.len();
result.push(c);
Some(EnvState::ProtoPlaceholder((pos, len_to_start)))
},
EnvState::ProtoPlaceholder(_) if c == enabler=> {
Some(EnvState::Escaped)
},
EnvState::Escaped if c == start => {
result.push(c);
Some(EnvState::Text)
},
EnvState::Escaped => {
result.push(enabler);
result.push(c);
Some(EnvState::Text)
},
EnvState::ProtoPlaceholder(start_pos) if c == start => {
result.push(c);
Some(EnvState::InPlaceholder(*start_pos))
},
EnvState::InPlaceholder(start_pos) if c == end => {
if start_pos.0 + 2 < pos {
result.truncate(start_pos.1);
let value = resolver(&input[(start_pos.0 + 2)..pos])?;
result.reserve(input.len() + value.len() - result.len());
result.push_str(&value);
} else {
result.push(c);
}
Some(EnvState::Text)
},
EnvState::ProtoPlaceholder(_) => {
result.push(c);
Some(EnvState::Text)
},
_ => {
result.push(c);
None
}
} {
state = next_state;
}
}
Ok(result)
}
pub trait Subst where Self: Sized {
fn env(self) -> Result<StringItem, ConfigError>;
fn expand(self, start: char, end: char, resolver: &dyn Fn(&str) -> Result<String, Box<dyn Error>>) -> Result<StringItem, ConfigError>;
}
impl Subst for Result<StringItem, ConfigError> {
fn env(self) -> Result<StringItem, ConfigError> {
self?.map(|v| {
let result = expand(v, '$', '{', '}', &|key| { Ok(env::var(key).unwrap_or_default()) } ).unwrap();
MapAction::Replace(vec!(result))
})
}
fn expand(self, start: char, end: char, resolver: &dyn Fn(&str) -> Result<String, Box<dyn Error>>) -> Result<StringItem, ConfigError> {
assert_ne!(start, '$');
assert_ne!(end, '$');
self?.map(|v| {
match expand(v, '$', start, end, resolver) {
Ok(result) => MapAction::Replace(vec!(result)),
Err(error) => MapAction::Fail(error)
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Config;
use crate::confpath::ConfPath;
use crate::item::ValueExtractor;
use crate::sources::defaults::Defaults;
use crate::error::ConfigError;
#[test]
fn explode() {
let mut c = Config::default();
let mut d = Defaults::default();
d.set(c.root().push_all(&["empty"]), "", "empty_test");
d.set(c.root().push_all(&["ten"]), "10", "10");
d.set(c.root().push_all(&["splitme"]), "1,2,3", "splitme");
d.set(c.root().push_all(&["multisplit"]), "1:2", "multisplit.1");
d.put(c.root().push_all(&["multisplit"]), "3:4:5", "multisplit.2");
c.add_source(d);
let values: Vec<u32> = c.get(ConfPath::from(&["splitme"])).explode(',').values(..).unwrap();
assert_eq!(values.len(), 3);
assert_eq!(values[0], 1);
assert_eq!(values[1], 2);
assert_eq!(values[2], 3);
let values: Vec<u32> = c.get(ConfPath::from(&["ten"])).explode(',').values(..).unwrap();
assert_eq!(values.len(), 1);
assert_eq!(values[0], 10);
let values: Vec<String> = c.get(ConfPath::from(&["empty"])).explode(',').values(..).unwrap();
assert_eq!(values.len(), 1);
assert!(values[0].is_empty());
let values: Vec<u32> = c.get(ConfPath::from(&["multisplit"])).explode(':').values(..).unwrap();
assert_eq!(values.len(), 5);
assert_eq!(values[0], 1);
assert_eq!(values[1], 2);
assert_eq!(values[2], 3);
assert_eq!(values[3], 4);
assert_eq!(values[4], 5);
}
#[test]
fn trim() {
let mut c = Config::default();
let mut d = Defaults::default();
d.set(c.root().push_all(&["trim"]), " text ", "splitme");
d.set(c.root().push_all(&["trim_mixed"]), "\ttext ", "splitme");
c.add_source(d);
let value: String = c.get(ConfPath::from(&["trim"])).trim().value().unwrap();
assert_eq!(value, "text");
let value: String = c.get(ConfPath::from(&["trim"])).trim_start().value().unwrap();
assert_eq!(value, "text ");
let value: String = c.get(ConfPath::from(&["trim"])).trim_end().value().unwrap();
assert_eq!(value, " text");
let value: String = c.get(ConfPath::from(&["trim_mixed"])).trim().value().unwrap();
assert_eq!(value, "text");
}
#[test]
fn unescape() {
let mut c = Config::default();
let mut d = Defaults::default();
d.set(c.root().push_all(&["standard"]), "\\r\\n\\t", "standard");
d.set(c.root().push_all(&["with_text"]), "rrr\\rnnn\\nttt\\t", "standard");
d.set(c.root().push_all(&["unknown"]), "\\x\\y\\z", "unknown");
d.set(c.root().push_all(&["at_end"]), "Text\\", "at_end");
c.add_source(d);
let value: String = c.get(ConfPath::from(&["standard"])).unescape().value().unwrap();
assert_eq!(value, "\r\n\t");
let value: String = c.get(ConfPath::from(&["with_text"])).unescape().value().unwrap();
assert_eq!(value, "rrr\rnnn\nttt\t");
let value: String = c.get(ConfPath::from(&["unknown"])).unescape().value().unwrap();
assert_eq!(value, "xyz");
let value: String = c.get(ConfPath::from(&["at_end"])).unescape().value().unwrap();
assert_eq!(value, "Text\\");
}
#[test]
fn not_empty() {
let mut c = Config::default();
let mut d = Defaults::default();
d.set(c.root().push_all(&["some_empty"]), "some_empty", "some_empty1");
d.put(c.root().push_all(&["some_empty"]), "", "some_empty2");
d.put(c.root().push_all(&["some_empty"]), " ", "some_empty3");
d.put(c.root().push_all(&["some_empty"]), "not_empty", "some_empty4");
d.set(c.root().push_all(&["all_empty"]), "", "all_empty1");
d.put(c.root().push_all(&["all_empty"]), "", "all_empty2");
c.add_source(d);
let mut values: Vec<String> = c.get(ConfPath::from(&["some_empty"])).not_empty().values(..).unwrap();
assert_eq!(values.len(), 2);
assert_eq!(values.pop().unwrap(), "not_empty");
assert_eq!(values.pop().unwrap(), "some_empty");
let values: Vec<String> = c.get(ConfPath::from(&["all_empty"])).not_empty().values(..).unwrap();
assert_eq!(values.len(), 0);
}
#[test]
fn unquote() {
let mut c = Config::default();
let mut d = Defaults::default();
d.set(c.root().push_all(&["quote"]), "\"test\"", "quote");
d.set(c.root().push_all(&["no_quote"]), "test", "no_quote");
d.set(c.root().push_all(&["start_quote"]), "\"test", "start_quote");
d.set(c.root().push_all(&["end_quote"]), "test\"", "end_quote");
c.add_source(d);
let value: String = c.get(ConfPath::from(&["quote"])).unquote().value().unwrap();
assert_eq!(value, "test");
assert!((c.get(ConfPath::from(&["no_quote"])).unquote().value() as Result<String, ConfigError>).is_err());
assert!((c.get(ConfPath::from(&["start_quote"])).unquote().value() as Result<String, ConfigError>).is_err());
assert!((c.get(ConfPath::from(&["end_quote"])).unquote().value() as Result<String, ConfigError>).is_err());
}
#[test]
#[should_panic(expected = "MissingQuotes")]
fn unquote_error() {
let mut c = Config::default();
let mut d = Defaults::default();
d.set(c.root().push_all(&["missing_end_quote"]), "\"test", "start_quote");
c.add_source(d);
let _: String = c.get(ConfPath::from(&["missing_end_quote"])).unquote().value().unwrap();
}
#[test]
fn env() {
let mut c = Config::default();
let mut d = Defaults::default();
d.set(c.root().push_all(&["env"]), "env=${TEST_ENV}", "env");
d.set(c.root().push_all(&["env_multiple"]), "a_first=${TEST_ENV} b_second=${TEST_ENV_SECOND} c_missing=${MISSING_ENV}", "env_multiple");
d.set(c.root().push_all(&["env_missing"]), "env=${MISSING_ENV}", "missing_env");
d.set(c.root().push_all(&["env_empty"]), "env=${}", "env_test");
d.set(c.root().push_all(&["env_escape"]), "env=$${NO_REPLACE}", "env_escape");
d.set(c.root().push_all(&["env_fake_escape"]), "cash=20$$$", "env_fake_escape");
d.set(c.root().push_all(&["env_unclosed"]), "env=${UNCLOSED", "env_unclosed");
d.set(c.root().push_all(&["env_special"]), "env=${${ENV}}", "env_special");
c.add_source(d);
env::set_var("TEST_ENV", "asdf");
env::set_var("TEST_ENV_SECOND", "xyz");
let value: String = c.get(ConfPath::from(&["env"])).env().value().unwrap();
assert_eq!(value, "env=asdf");
let value: String = c.get(ConfPath::from(&["env_multiple"])).env().value().unwrap();
assert_eq!(value, "a_first=asdf b_second=xyz c_missing=");
let value: String = c.get(ConfPath::from(&["env_missing"])).env().value().unwrap();
assert_eq!(value, "env=");
let value: String = c.get(ConfPath::from(&["env_empty"])).env().value().unwrap();
assert_eq!(value, "env=${}");
let value: String = c.get(ConfPath::from(&["env_escape"])).env().value().unwrap();
assert_eq!(value, "env=${NO_REPLACE}");
let value: String = c.get(ConfPath::from(&["env_fake_escape"])).env().value().unwrap();
assert_eq!(value, "cash=20$$$");
let value: String = c.get(ConfPath::from(&["env_unclosed"])).env().value().unwrap();
assert_eq!(value, "env=${UNCLOSED");
let value: String = c.get(ConfPath::from(&["env_special"])).env().value().unwrap();
assert_eq!(value, "env=}");
}
#[test]
fn expand() {
let mut c = Config::default();
let mut d = Defaults::default();
d.set(c.root().push_all(&["round_br"]), "env=$(TEST)", "round_br");
d.set(c.root().push_all(&["square_br"]), "env=$[TEST]", "square_br");
d.set(c.root().push_all(&["same_start_end"]), "env=$|TEST|", "same_start_end");
c.add_source(d);
let resolver_ok = |key: &str| { assert_eq!(key, "TEST"); Ok(String::from("asdf")) };
let resolver_err: &dyn Fn(&str) -> Result<String, Box<dyn Error>> = &|_: &str| { Err(Box::new(std::env::VarError::NotPresent)) };
let value: String = c.get(ConfPath::from(&["round_br"])).expand('(', ')', &resolver_ok).value().unwrap();
assert_eq!(value, "env=asdf");
let value: String = c.get(ConfPath::from(&["square_br"])).expand('[', ']', &resolver_ok).value().unwrap();
assert_eq!(value, "env=asdf");
let value: String = c.get(ConfPath::from(&["same_start_end"])).expand('|', '|', &resolver_ok).value().unwrap();
assert_eq!(value, "env=asdf");
assert!((c.get(ConfPath::from(&["round_br"])).expand('(', ')', resolver_err).value() as Result<String, ConfigError>).is_err());
}
#[test]
fn self_resolve() {
let mut c = Config::default();
let mut d = Defaults::default();
d.set(c.root().push_all(&["expand_me"]), "env=${test}", "round_br");
d.set(c.root().push_all(&["test"]), "asdf", "square_br");
c.add_source(d);
let resolver = |key: &str| { (c.get(c.root().push_all(key.split('.'))).value() as Result<String, ConfigError>).map_err(Box::from) };
let value: String = c.get(ConfPath::from(&["expand_me"])).expand('{', '}', &resolver).value().unwrap();
assert_eq!(value, "env=asdf");
}
}