justconfig 1.0.1

Just a configuration information source for rust
Documentation
//! Processors trim, split, unescape or otherwise process the configuration items
//! before they get parsed into typed values.
//!
//! Processors are used to modify configuration items. For an introduction to
//! configuration items see the documentation for the [`item`](crate::item)
//! module.
//!
//! Processors are implemented as the combination of a trait and an implementation.
//! The trait defines the methods that the processor provides and is always
//! implemented for `Result<StringItem, ConfigError>`. Each processor method takes
//! an owned `self` and returns `Result<StringItem, ConfigError>`. That way
//! processors can be easily chained.
//!
//! To use a processor just put it after the [`get`](crate::Config::get)
//! method of the [`Config`](crate::Config) struct.
//!
//! ```rust
//! # use justconfig::Config;
//! # use justconfig::ConfPath;
//! # use justconfig::item::ValueExtractor;
//! # use justconfig::sources::defaults::Defaults;
//! # use justconfig::processors::Trim;
//! # let mut conf = Config::default();
//! # let mut defaults = Defaults::default();
//! defaults.set(conf.root().push_all(&["myvalue"]), "abc", "source info");
//! conf.add_source(defaults);
//!
//! let trimed_value: String = conf.get(ConfPath::from(&["myvalue"])).trim().value().unwrap();
//! ```
//!
//! ## Implementing a processor
//!
//! To implement a new processor first have a look at the [source](crate::processors)
//! of the existing processors.
//!
//! For processors there is a helper method within the
//! [`Item`](crate::item::StringItem) struct. This method is called
//! [`map`](crate::item::StringItem#map).
//!
//! The processor first checks, if there is an error value within the `Result`.
//! If there is one, the error is returned without further processing.
//! Then `map` is called and the result of the mapping operation is returned to
//! the next step of the pipeline. A basic processor looks like this:
//!
//! ```rust
//! use justconfig::error::ConfigError;
//! use justconfig::item::{StringItem, MapAction};
//!
//! pub trait Frobnify where Self: Sized {
//!   fn frobnify(self, frob_count: u8) -> Result<StringItem, ConfigError>;
//! }
//!
//! impl Frobnify for Result<StringItem, ConfigError> {
//!   fn frobnify(self, frob_count: u8) -> Result<StringItem, ConfigError> {
//!     self?.map(|v| {
//!       // Your code goes here.
//! #     MapAction::Keep
//!     })
//!   }
//! }
//! ```
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 {
}

/// Splits a character delimited config value into multiple configuration values.
pub trait Explode where Self: Sized {
	//TODO: Make char a pattern as soon as this is stable
	fn explode(self, delimiter: char) -> Result<StringItem, ConfigError>;
}

impl Explode for Result<StringItem, ConfigError> {
	/// Call this method on the configuration pipeline to split a config value into multiple values.
	///
	/// The passed delimiter is used as a separator for the configuration values.
	///
	/// ## Example
	///
	/// ```rust
	/// # use justconfig::Config;
	/// # use justconfig::ConfPath;
	/// # use justconfig::item::ValueExtractor;
	/// # use justconfig::sources::defaults::Defaults;
	/// # use justconfig::processors::Explode;
	/// #
	/// # let mut conf = Config::default();
	/// # let mut defaults = Defaults::default();
	/// defaults.set(conf.root().push_all(&["splitme"]), "1,2,3", "source info");
	/// conf.add_source(defaults);
	///
	/// let values: Vec<u32> = conf.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);
	/// ```
	fn explode(self, delimiter: char) -> Result<StringItem, ConfigError> {
		self?.map(|v| {
			MapAction::Replace(Vec::from_iter(v.split(delimiter).map(|v| {
				String::from(v)
			})))
		})
	}
}

/// Trims leading, trailing or leading and trailing whitespaces from all config values.
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> {
	/// Trims leading and trailing whitespaces from all config values.
	///
	/// This methods calls `String::trim()` for all config values of the current
	/// configuration item.
	///
	/// ## Example
	///
	/// ```rust
	/// # use justconfig::Config;
	/// # use justconfig::ConfPath;
	/// # use justconfig::item::ValueExtractor;
	/// # use justconfig::sources::defaults::Defaults;
	/// # use justconfig::processors::Trim;
	/// #
	/// # let mut conf = Config::default();
	/// # let mut defaults = Defaults::default();
	/// defaults.set(conf.root().push_all(&["myitem"]), "   abc\t", "source info");
	/// conf.add_source(defaults);
	///
	/// let value: String = conf.get(ConfPath::from(&["myitem"])).trim().value().unwrap();
	///
	/// assert_eq!(value, "abc");
	/// ```
	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
			}
		})
	}

	/// Trims leading whitespaces from all configuration value.
	///
	/// This methods calls `String::trim_start()` for all config values of the current
	/// configuration item.
	///
	/// ## Example
	///
	/// ```rust
	/// # use justconfig::Config;
	/// # use justconfig::ConfPath;
	/// # use justconfig::item::ValueExtractor;
	/// # use justconfig::sources::defaults::Defaults;
	/// # use justconfig::processors::Trim;
	/// #
	/// # let mut conf = Config::default();
	/// # let mut defaults = Defaults::default();
	/// defaults.set(conf.root().push_all(&["myitem"]), "   abc   ", "source info");
	/// conf.add_source(defaults);
	///
	/// let value: String = conf.get(ConfPath::from(&["myitem"])).trim_start().value().unwrap();
	///
	/// // Note that the trailing whitespaces where kept.
	/// assert_eq!(value, "abc   ");
	/// ```
	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
			}
		})
	}

	/// Trims trailing whitespaces from all configuration value.
	///
	/// This methods calls `String::trim_end()` for all config values of the current
	/// configuration item.
	///
	/// ## Example
	///
	/// ```rust
	/// # use justconfig::Config;
	/// # use justconfig::ConfPath;
	/// # use justconfig::item::ValueExtractor;
	/// # use justconfig::sources::defaults::Defaults;
	/// # use justconfig::processors::Trim;
	/// #
	/// # let mut conf = Config::default();
	/// # let mut defaults = Defaults::default();
	/// defaults.set(conf.root().push_all(&["myitem"]), "   abc   ", "source info");
	/// conf.add_source(defaults);
	///
	/// let value: String = conf.get(ConfPath::from(&["myitem"])).trim_end().value().unwrap();
	///
	/// // Note that the leading whitespaces where kept.
	/// assert_eq!(value, "   abc");
	/// ```
	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
			}
		})
	}
}

/// Convert escape sequences to special characters.
pub trait Unescape where Self: Sized {
	fn unescape(self) -> Result<StringItem, ConfigError>;
}

impl Unescape for Result<StringItem, ConfigError> {
	/// Call this method to convert escaped control characters to real control characters.
	///
	/// The following control characters can be used:
	///
	/// * `\n`
	/// * `\r`
	/// * `\t`
	///
	/// ## Example
	///
	/// ```rust
	/// # use justconfig::Config;
	/// # use justconfig::ConfPath;
	/// # use justconfig::item::ValueExtractor;
	/// # use justconfig::sources::defaults::Defaults;
	/// # use justconfig::processors::Unescape;
	/// #
	/// # let mut conf = Config::default();
	/// # let mut defaults = Defaults::default();
	/// defaults.set(conf.root().push_all(&["myitem"]), r#"\r\n"#, "source info");
	/// conf.add_source(defaults);
	///
	/// let value: String = conf.get(ConfPath::from(&["myitem"])).unescape().value().unwrap();
	///
	/// assert_eq!(value, "\r\n");
	/// ```
	fn unescape(self) -> Result<StringItem, ConfigError> {
		self?.map(|v| {
			let mut output = String::with_capacity(v.len() + 10);	// We assume that there are not more than 10 Escaped characters per line.

			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))
		})
	}
}

/// Removes empty config values.
pub trait NotEmpty where Self: Sized {
	fn not_empty(self) -> Result<StringItem, ConfigError>;
}

impl NotEmpty for Result<StringItem, ConfigError> {
	/// Call this method to remove all empty configuration values from a configuration item.
	///
	/// ## Example
	///
	/// ```rust
	/// # use justconfig::Config;
	/// # use justconfig::ConfPath;
	/// # use justconfig::item::ValueExtractor;
	/// # use justconfig::sources::defaults::Defaults;
	/// # use justconfig::processors::NotEmpty;
	/// #
	/// # let mut conf = Config::default();
	/// # let mut defaults = Defaults::default();
	/// defaults.set(conf.root().push_all(&["myitem"]), "abc", "source info");
	/// defaults.put(conf.root().push_all(&["myitem"]), "", "source info");
	/// defaults.put(conf.root().push_all(&["myitem"]), "def", "source info");
	/// conf.add_source(defaults);
	///
	/// let values: Vec<String> = conf.get(ConfPath::from(&["myitem"])).not_empty().values(..).unwrap();
	///
	/// assert_eq!(values.len(), 2);
	/// assert_eq!(values[0], "abc");
	/// assert_eq!(values[1], "def");
	/// ```
	fn not_empty(self) -> Result<StringItem, ConfigError> {
		self?.map(|v| {
			if v.trim().is_empty() {
				MapAction::Drop
			} else {
				MapAction::Keep
			}
		})
	}
}

/// Remove quotes from configuration strings.
pub trait Unquote where Self: Sized {
	fn unquote(self) -> Result<StringItem, ConfigError>;
}

impl Unquote for Result<StringItem, ConfigError> {
	/// Call this method to remove quotes around all configuration values.
	///
	/// All configuration values will automatically be trimmed and checked for a
	/// loading and trailing quote (`"`). If the quote is there, it will be
	/// removed. If it's missing a `ProcessingError::MissingQuotes` error will be
	/// generated.
	///
	/// ## Example
	///
	/// ```rust
	/// # use justconfig::Config;
	/// # use justconfig::ConfPath;
	/// # use justconfig::item::ValueExtractor;
	/// # use justconfig::sources::defaults::Defaults;
	/// # use justconfig::processors::Unquote;
	/// #
	/// # let mut conf = Config::default();
	/// # let mut defaults = Defaults::default();
	/// defaults.set(conf.root().push_all(&["quoted"]), "\"abc\"", "source info");
	/// conf.add_source(defaults);
	///
	/// let value: String = conf.get(ConfPath::from(&["quoted"])).unquote().value().unwrap();
	///
	/// assert_eq!(value, "abc");
	/// ```
	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))
			}
		})
	}
}

/// Expands an input string by calling a resolver function for each placeholder.
///
/// The `enabler` character starts the placeholder. The next character must be
/// the `start` character. The `end` character will terminate the placeholder.
///
/// Repeating the `enabler` character two times serves as an escape sequence to
/// allow the combination `enabler` + `start` as normal text. The duplicate
/// `enabler` character will be removed. The duplicate `enabler`character will
/// only be removed if it is followed by the start character.
///
/// The key between the `start` and the `end` character will be passed to the
/// resolver function. The result of the resolver function will be used to
/// replace the placeholder. If the resolver function returns an error, processing
/// will stop and the error will be returned.
///
/// This function will never generate an error by itself. If the `resolver`
/// function does not return an error, no error will ever be returned.
///
/// ## Example
/// If `enabler` is `$` and `start` is `{` the sequence `$${` will output `${`.
/// The sequence `$$a` will output `$$a`.
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
		{
			// If we detect an enabler char in normal text we enter the
			// ProtoPlaceholder state.
			EnvState::Text if c == enabler => {
				let len_to_start = result.len();

				result.push(c);

				Some(EnvState::ProtoPlaceholder((pos, len_to_start)))
			},
			// If a second $ character appears while in ProtoPlaceholder
			// state we've detected the $$ escape and swallow the second $.
			// We advance the state to Escaped now it depends on the
			// next character what happens.
			EnvState::ProtoPlaceholder(_) if c == enabler=> {
				Some(EnvState::Escaped)
			},
			// Two $ character where detected before this { character.
			// The first $ was already put into the output stream. The
			// second one will be swallowed as an escape character.
			EnvState::Escaped if c == start => {
				result.push(c);

				Some(EnvState::Text)
			},
			// Two $ character where detected but the next character was
			// not {. We reinsert the $ sign because it was not am
			// escape character.
			EnvState::Escaped => {
				result.push(enabler);
				result.push(c);

				Some(EnvState::Text)
			},
			// If we're in proto placeholder state and get a { we are inside
			// a placeholder.
			EnvState::ProtoPlaceholder(start_pos) if c == start => {
				result.push(c);

				Some(EnvState::InPlaceholder(*start_pos))
			},
			// If we're inside a placeholder and receive a } we have reached
			// the end of the placeholder an can process it.
			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])?;

					// Extend the string by the length of the value.
					// This calculation is a little strange because we have to
					// pass the number of bytes that we want to reserve in
					// addition to the current length.
					result.reserve(input.len() + value.len() - result.len());

					result.push_str(&value);
				} else {
					result.push(c);
				}

				Some(EnvState::Text)
			},
			// If we're in proto placeholder state and receive any unknown
			// character we return to text state.
			EnvState::ProtoPlaceholder(_) => {
				result.push(c);

				Some(EnvState::Text)
			},
			// Any other character is simply copied.
			_ => {
				result.push(c);
				None
			}
		} {
			// If a new state was returned, update the state variable
			state = next_state;
		}
	}

	Ok(result)
}

/// Substitute placeholders within config values with values (for example
/// environment variables).
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> {
	/// Call this method to substitute placeholders with environment variables.
	///
	/// An environment variable can be referenced by `${name}`. Every occurrence of
	/// of this placeholder is expanded by replacing it with the named environment
	/// variable. If the environment variable is not set or can not be converted into
	/// a UTF-8 string an empty string is substituted.
	///
	/// To escape the start sequence `${` a second `$` character must be used.
	/// For example `$${LITERAL}` will be replaced by `${LITERAL}` without expanding
	/// the environment variable `LITERAL`. Any `$` character not followed by `{`
	/// must not be escaped. The string `cash: $$$` will be returned as `cash: $$$`.
	///
	/// ## Example
	///
	/// ```rust
	/// # use justconfig::Config;
	/// # use justconfig::ConfPath;
	/// # use justconfig::item::ValueExtractor;
	/// # use justconfig::sources::defaults::Defaults;
	/// # use justconfig::processors::Subst;
	/// #
	/// # let mut conf = Config::default();
	/// # let mut defaults = Defaults::default();
	/// defaults.set(conf.root().push_all(&["env"]), "${PATH}", "substitute PATH");
	/// conf.add_source(defaults);
	///
	/// let value: String = conf.get(ConfPath::from(&["env"])).env().value().unwrap();
	///
	/// assert_eq!(value, std::env::var("PATH").unwrap_or_default());
	/// ```
	fn env(self) -> Result<StringItem, ConfigError> {
		self?.map(|v| {
			// Unwrap can be called here because we always return ok from the resolver closure.
			// ToDo: Use into_ok() as soon as it's stable.
			let result = expand(v, '$', '{', '}', &|key| { Ok(env::var(key).unwrap_or_default()) } ).unwrap();

			MapAction::Replace(vec!(result))
		})
	}

	/// Call this method to substitute placeholders with an application defined
	/// value.
	///
	/// For a general description see the [`env`](crate::processors::Subst::env) method. This
	/// method is more general than `env` as it allows the characters enclosing
	/// the variable to be set and uses a callback to supply the value that
	/// should be substituted.
	///
	/// Because this function allows the enclosing characters to be set
	/// different substitutions can be used for different sources of the
	/// substituted value. For example `${}` can be sued for environment
	/// variable substitution and `$()` could be used for substitution for a
	/// secondary configuration file.
	/// 
	/// The `$` character as the start marker can not be changed.
	///
	/// ## Example
	///
	/// This example emulates the [`env`](crate::processors::Subst::env) method but returns an
	/// error if the environment variable is not found. In addition it replaces
	/// the curly brackets used by the `env` method with round ones.
	///
	/// ```rust
	/// # use std::error::Error;
	/// # use std::env;
	/// # use justconfig::Config;
	/// # use justconfig::ConfPath;
	/// # use justconfig::error::ConfigError;
	/// # use justconfig::item::ValueExtractor;
	/// # use justconfig::sources::defaults::Defaults;
	/// # use justconfig::processors::Subst;
	/// #
	/// # let mut conf = Config::default();
	/// # let mut defaults = Defaults::default();
	/// defaults.set(conf.root().push_all(&["env"]), "$(I_DONT_KONW)", "substitute PATH");
	/// conf.add_source(defaults);
	///
	/// let result: Result<String, ConfigError> = conf.get(ConfPath::from(&["env"])).expand('(', ')', &|key| { env::var(key).map_err(Box::from) } ).value();
	///
	/// assert!(result.is_err());
	/// ```
	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| {
			// Unwrap can be called here because we always return ok from the resolver closure.
			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);

		// This resolver checks if the passed key is "TEST". All tests use this key.
		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);

		// This resolver uses the config tree to resolve the passed key. That way the config system can refer to itself
		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");
	}
}