use std::{
any::{type_name, Any},
borrow::Borrow,
cell::RefCell,
collections::HashSet,
env::var,
path::PathBuf,
};
use crate::{
cache::CacheConfigSource,
err::ConfigError,
impl_cache,
key::{CacheString, ConfigKey, PartialKeyIter},
macros::{cfg_log, impl_default},
source::{
cargo::Cargo, environment::PrefixEnvironment, memory::HashSource, register_by_ext,
register_files, ConfigSource, SourceOption,
},
value::ConfigValue,
value_ref::Refresher,
FromConfig, FromConfigWithPrefix, PartialKeyCollector,
};
#[allow(missing_debug_implementations)]
pub struct ConfigContext<'a> {
key: ConfigKey<'a>,
source: &'a HashSource,
pub(crate) ref_value_flag: bool,
}
struct CacheValue {
buf: String,
stack: Vec<usize>,
}
impl CacheValue {
fn new() -> Self {
Self {
buf: String::with_capacity(10),
stack: Vec::with_capacity(3),
}
}
fn clear(&mut self) {
self.buf.clear();
self.stack.clear();
}
}
impl_cache!(CacheValue);
impl HashSource {
pub(crate) fn new_context<'a>(&'a self, cache: &'a mut CacheString) -> ConfigContext<'a> {
ConfigContext {
key: cache.new_key(),
source: self,
ref_value_flag: false,
}
}
}
impl<'a> ConfigContext<'a> {
pub(crate) fn as_refresher(&self) -> &Refresher {
&self.source.refs
}
fn parse_placeholder(
source: &'a HashSource,
current_key: &ConfigKey<'_>,
val: &str,
history: &mut HashSet<String>,
) -> Result<(bool, Option<ConfigValue<'a>>), ConfigError> {
let pat: &[_] = &['$', '\\', '}'];
CacheValue::with_key(move |cv| {
cv.clear();
let mut value = val;
let mut flag = true;
while let Some(pos) = value.find(pat) {
flag = false;
match &value[pos..=pos] {
"$" => {
let pos_1 = pos + 1;
if value.len() == pos_1 || &value[pos_1..=pos_1] != "{" {
return Err(ConfigError::ConfigParseError(
current_key.to_string(),
val.to_owned(),
));
}
cv.buf.push_str(&value[..pos]);
cv.stack.push(cv.buf.len());
value = &value[pos + 2..];
}
"\\" => {
let pos_1 = pos + 1;
if value.len() == pos_1 {
return Err(ConfigError::ConfigParseError(
current_key.to_string(),
val.to_owned(),
));
}
cv.buf.push_str(&value[..pos]);
cv.buf.push_str(&value[pos_1..=pos_1]);
value = &value[pos + 2..];
}
"}" => {
let last = cv.stack.pop().ok_or_else(|| {
ConfigError::ConfigParseError(current_key.to_string(), val.to_owned())
})?;
cv.buf.push_str(&value[..pos]);
let v = &(cv.buf.as_str())[last..];
let (key, def) = match v.find(':') {
Some(pos) => (&v[..pos], Some(&v[pos + 1..])),
_ => (v, None),
};
if !history.insert(key.to_string()) {
return Err(ConfigError::ConfigRecursiveError(current_key.to_string()));
}
let v = match CacheString::with_key_place(|cache| {
source
.new_context(cache)
.do_parse_config::<String, &str>(key, None, history)
}) {
Err(ConfigError::ConfigNotFound(v)) => match def {
Some(v) => v.to_owned(),
_ => return Err(ConfigError::ConfigRecursiveNotFound(v)),
},
ret => ret?,
};
history.remove(key);
cv.buf.truncate(last);
cv.buf.push_str(&v);
value = &value[pos + 1..];
}
_ => return Err(ConfigError::ConfigRecursiveError(current_key.to_string())),
}
}
if flag {
return Ok((true, None));
}
if cv.stack.is_empty() {
return Ok((false, Some(cv.buf.to_string().into())));
}
Err(ConfigError::ConfigParseError(
current_key.to_string(),
val.to_owned(),
))
})
}
#[inline]
pub(crate) fn do_parse_config<T: FromConfig, K: Into<PartialKeyIter<'a>>>(
&mut self,
partial_key: K,
default_value: Option<ConfigValue<'_>>,
history: &mut HashSet<String>,
) -> Result<T, ConfigError> {
let mark = self.key.push(partial_key);
let value = match self.source.get_value(&self.key).or(default_value) {
Some(ConfigValue::StrRef(s)) => {
match Self::parse_placeholder(self.source, &self.key, s, history)? {
(true, _) => Some(ConfigValue::StrRef(s)),
(false, v) => v,
}
}
Some(ConfigValue::Str(s)) => {
match Self::parse_placeholder(self.source, &self.key, &s, history)? {
(true, _) => Some(ConfigValue::Str(s)),
(_, v) => v,
}
}
#[cfg(feature = "rand")]
Some(ConfigValue::Rand(s)) => Some(s.normalize()),
v => v,
};
let v = T::from_config(self, value);
self.key.pop(mark);
v
}
#[inline]
pub fn parse_config<T: FromConfig>(
&mut self,
partial_key: &'a str,
default_value: Option<ConfigValue<'_>>,
) -> Result<T, ConfigError> {
self.do_parse_config(partial_key, default_value, &mut HashSet::new())
}
#[inline]
pub fn current_key(&self) -> String {
self.key.to_string()
}
#[allow(dead_code)]
pub(crate) fn current_key_str(&self) -> &str {
self.key.as_str()
}
#[inline]
pub(crate) fn type_mismatch<T: Any>(&self, value: &ConfigValue<'_>) -> ConfigError {
let tp = match value {
ConfigValue::StrRef(_) => "String",
ConfigValue::Str(_) => "String",
ConfigValue::Int(_) => "Integer",
ConfigValue::Float(_) => "Float",
ConfigValue::Bool(_) => "Bool",
#[cfg(feature = "rand")]
ConfigValue::Rand(_) => "Random",
};
ConfigError::ConfigTypeMismatch(self.current_key(), tp, type_name::<T>())
}
#[inline]
pub(crate) fn not_found(&self) -> ConfigError {
ConfigError::ConfigNotFound(self.current_key())
}
#[inline]
pub fn parse_error(&self, value: &str) -> ConfigError {
ConfigError::ConfigParseError(self.current_key(), value.to_owned())
}
pub(crate) fn collect_keys(&self) -> PartialKeyCollector<'a> {
let mut c = PartialKeyCollector::new();
self.source.collect_keys(&self.key, &mut c);
c
}
}
#[allow(missing_debug_implementations)]
pub struct Configuration {
pub(crate) source: HashSource,
max: usize,
loaders: Vec<Box<dyn ConfigSource + Send + 'static>>,
}
impl_default!(Configuration);
impl Configuration {
pub fn new() -> Self {
Self {
source: HashSource::new("configuration"),
max: 64,
loaders: vec![],
}
}
pub fn register_kv<N: Into<String>>(self, name: N) -> ManualSource {
ManualSource(self, HashSource::new(name))
}
pub fn register_prefix_env(self, prefix: &str) -> Result<Self, ConfigError> {
self.register_source(PrefixEnvironment::new(prefix))
}
pub fn register_file<P: Into<PathBuf>>(
self,
path: P,
required: bool,
) -> Result<Self, ConfigError> {
register_by_ext(self, path.into(), required)
}
#[cfg(feature = "rand")]
#[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
pub fn register_random(self) -> Result<Self, ConfigError> {
self.register_source(crate::source::random::Random)
}
pub fn register_source<L: ConfigSource + 'static>(
mut self,
loader: L,
) -> Result<Self, ConfigError> {
if self.max <= self.loaders.len() {
return Err(ConfigError::TooManyInstances(self.max));
}
let loader = CacheConfigSource::new(loader);
let builder = &mut self.source.prefixed();
loader.load(builder)?;
cfg_log!(
log::Level::Debug,
"Config source {}:{} registered.",
self.loaders.len() + 1,
loader.name()
);
self.loaders.push(Box::new(loader));
Ok(self)
}
#[inline]
fn reload(&self) -> Result<(bool, Configuration), ConfigError> {
let mut s = Configuration::new();
let mut refreshed = false;
for i in self.loaders.iter() {
if i.refreshable()? {
refreshed = true;
}
}
if refreshed {
let c = &mut s.source.prefixed();
for i in self.loaders.iter() {
i.load(c)?;
}
self.source.refs.refresh(&s)?;
cfg_log!(log::Level::Info, "Configuration refreshed");
}
Ok((refreshed, s))
}
pub fn refresh_ref(&self) -> Result<bool, ConfigError> {
Ok(self.reload()?.0)
}
pub fn refresh(&mut self) -> Result<bool, ConfigError> {
let (x, c) = self.reload()?;
if x {
self.source.value = c.source.value;
}
Ok(x)
}
#[inline]
pub fn get<T: FromConfig>(&self, key: &str) -> Result<T, ConfigError> {
CacheString::with_key(|cache| {
let mut context = self.source.new_context(cache);
context.parse_config(key, None)
})
}
#[inline]
pub fn get_or<T: FromConfig>(&self, key: &str, def: T) -> Result<T, ConfigError> {
Ok(self.get::<Option<T>>(key)?.unwrap_or(def))
}
#[inline]
pub fn get_predefined<T: FromConfigWithPrefix>(&self) -> Result<T, ConfigError> {
self.get(T::prefix())
}
pub fn source_names(&self) -> Vec<&str> {
self.loaders.iter().map(|l| l.name()).collect()
}
pub fn with_predefined_builder() -> PredefinedConfigurationBuilder {
PredefinedConfigurationBuilder {
memory: HashSource::new("fixed:FromProgram/CommandLineArgs"),
cargo: None,
prefix: None,
init: None,
}
}
pub fn with_predefined() -> Result<Self, ConfigError> {
Self::with_predefined_builder().init()
}
}
#[allow(clippy::type_complexity, missing_debug_implementations)]
pub struct PredefinedConfigurationBuilder {
memory: HashSource,
cargo: Option<Cargo>,
prefix: Option<String>,
init: Option<Box<dyn FnOnce(&Configuration) -> Result<(), ConfigError> + 'static>>,
}
impl PredefinedConfigurationBuilder {
pub fn set_prefix_env<K: ToString>(mut self, prefix: K) -> Self {
self.prefix = Some(prefix.to_string());
self
}
pub fn set_dir<V: Into<PathBuf>>(self, path: V) -> Self {
self.set("app.dir", path.into().display().to_string())
}
pub fn set_name<V: Into<String>>(self, name: V) -> Self {
self.set("app.name", name.into())
}
pub fn set_profile<V: Into<String>>(self, profile: V) -> Self {
self.set("app.profile", profile.into())
}
pub fn set<K: Borrow<str>, V: Into<ConfigValue<'static>>>(mut self, key: K, value: V) -> Self {
self.memory = self.memory.set(key, value);
self
}
pub fn set_cargo_env(mut self, cargo: Cargo) -> Self {
self.cargo = Some(cargo);
self
}
pub fn set_init<F: FnOnce(&Configuration) -> Result<(), ConfigError> + 'static>(
mut self,
f: F,
) -> Self {
self.init = Some(Box::new(f));
self
}
pub fn init(self) -> Result<Configuration, ConfigError> {
let mut config = Configuration::new();
if let Some(cargo) = self.cargo {
config = config.register_source(cargo)?;
}
config = config.register_source(self.memory)?;
let option: SourceOption = config.get_predefined()?;
#[cfg(feature = "rand")]
if option.random.enabled {
config = config.register_random()?;
}
let prefix = self
.prefix
.or_else(|| config.get::<Option<String>>("env.prefix").ok().flatten())
.or_else(|| var("CFG_ENV_PREFIX").ok())
.unwrap_or_else(|| "CFG".to_owned());
config = config.register_prefix_env(&prefix)?;
if let Some(init) = self.init {
(init)(&config)?;
cfg_log!(log::Level::Info, "Early initialization completed.");
}
let app = config.get_predefined::<AppConfig>()?;
let mut path = PathBuf::new();
if let Some(d) = app.dir {
path.push(d);
};
if let Some(profile) = &app.profile {
let mut path = path.clone();
path.push(format!("{}-{}", app.name, profile));
config = register_files(config, &option, path, false)?;
}
path.push(app.name);
config = register_files(config, &option, path, false)?;
cfg_log!(
log::Level::Info,
"Predefined configuration initialization completed."
);
Ok(config)
}
}
#[derive(Debug, FromConfig)]
#[config(prefix = "app")]
struct AppConfig {
#[config(default = "app")]
name: String,
dir: Option<String>,
profile: Option<String>,
}
#[allow(missing_debug_implementations)]
pub struct ManualSource(Configuration, HashSource);
impl ManualSource {
pub fn set<K: Borrow<str>, V: Into<ConfigValue<'static>>>(mut self, key: K, value: V) -> Self {
self.0.source = self.0.source.set(key, value);
self
}
pub fn finish(self) -> Result<Configuration, ConfigError> {
self.0.register_source(self.1)
}
}
#[cfg(test)]
mod test {
use crate::{init_cargo_env, test::TestConfigExt};
use super::*;
macro_rules! should_eq {
($context:ident: $val:literal as $t:ty = $x:expr ) => {
println!("{} key: {}", type_name::<$t>(), $val);
assert_eq!($x, &format!("{:?}", $context.get::<$t>($val)));
};
}
fn build_config() -> Configuration {
HashSource::new("test")
.set("a", "0")
.set("b", "${b}")
.set("c", "${a}")
.set("d", "${z}")
.set("e", "${z:}")
.set("f", "${z:${a}}")
.set("g", "a")
.set("h", "${${g}}")
.set("i", "\\$\\{a\\}")
.set("j", "${${g}:a}")
.set("k", "${a} ${a}")
.set("l", "${c}")
.set("m", "${no_found:${no_found_2:hello}}")
.set("n", "$")
.set("o", "\\")
.set("p", "}")
.set("q", "${")
.new_config()
.register_kv("test")
.set("a0", "0")
.set("a", "1")
.set("b", "1")
.set("c", "1")
.finish()
.unwrap()
}
#[test]
fn parse_string_test() {
let config = build_config();
should_eq!(config: "a0" as String = "Ok(\"0\")");
should_eq!(config: "a" as String = "Ok(\"0\")");
should_eq!(config: "b" as String = "Err(ConfigRecursiveError(\"b\"))");
should_eq!(config: "c" as String = "Ok(\"0\")");
should_eq!(config: "d" as String = "Err(ConfigRecursiveNotFound(\"z\"))");
should_eq!(config: "e" as String = "Ok(\"\")");
should_eq!(config: "f" as String = "Ok(\"0\")");
should_eq!(config: "g" as String = "Ok(\"a\")");
should_eq!(config: "h" as String = "Ok(\"0\")");
should_eq!(config: "i" as String = "Ok(\"${a}\")");
should_eq!(config: "j" as String = "Ok(\"0\")");
should_eq!(config: "k" as String = "Ok(\"0 0\")");
should_eq!(config: "l" as String = "Ok(\"0\")");
should_eq!(config: "m" as String = "Ok(\"hello\")");
should_eq!(config: "n" as String = "Err(ConfigParseError(\"n\", \"$\"))");
should_eq!(config: "o" as String = "Err(ConfigParseError(\"o\", \"\\\\\"))");
should_eq!(config: "p" as String = "Err(ConfigParseError(\"p\", \"}\"))");
should_eq!(config: "q" as String = "Err(ConfigParseError(\"q\", \"${\"))");
}
#[test]
fn parse_bool_test() {
let config = build_config();
should_eq!(config: "a0" as bool = "Err(ConfigParseError(\"a0\", \"0\"))");
should_eq!(config: "a" as bool = "Err(ConfigParseError(\"a\", \"0\"))");
should_eq!(config: "b" as bool = "Err(ConfigRecursiveError(\"b\"))");
should_eq!(config: "c" as bool = "Err(ConfigParseError(\"c\", \"0\"))");
should_eq!(config: "d" as bool = "Err(ConfigRecursiveNotFound(\"z\"))");
should_eq!(config: "e" as bool = "Err(ConfigNotFound(\"e\"))");
should_eq!(config: "f" as bool = "Err(ConfigParseError(\"f\", \"0\"))");
should_eq!(config: "g" as bool = "Err(ConfigParseError(\"g\", \"a\"))");
should_eq!(config: "h" as bool = "Err(ConfigParseError(\"h\", \"0\"))");
should_eq!(config: "i" as bool = "Err(ConfigParseError(\"i\", \"${a}\"))");
should_eq!(config: "j" as bool = "Err(ConfigParseError(\"j\", \"0\"))");
should_eq!(config: "k" as bool = "Err(ConfigParseError(\"k\", \"0 0\"))");
should_eq!(config: "l" as bool = "Err(ConfigParseError(\"l\", \"0\"))");
should_eq!(config: "m" as bool = "Err(ConfigParseError(\"m\", \"hello\"))");
should_eq!(config: "n" as bool = "Err(ConfigParseError(\"n\", \"$\"))");
should_eq!(config: "o" as bool = "Err(ConfigParseError(\"o\", \"\\\\\"))");
should_eq!(config: "p" as bool = "Err(ConfigParseError(\"p\", \"}\"))");
should_eq!(config: "q" as bool = "Err(ConfigParseError(\"q\", \"${\"))");
}
#[test]
fn parse_u8_test() {
let config = build_config();
should_eq!(config: "a0" as u8 = "Ok(0)");
should_eq!(config: "a" as u8 = "Ok(0)");
should_eq!(config: "b" as u8 = "Err(ConfigRecursiveError(\"b\"))");
should_eq!(config: "c" as u8 = "Ok(0)");
should_eq!(config: "d" as u8 = "Err(ConfigRecursiveNotFound(\"z\"))");
should_eq!(config: "e" as u8 = "Err(ConfigNotFound(\"e\"))");
should_eq!(config: "f" as u8 = "Ok(0)");
should_eq!(config: "g" as u8 = "Err(ConfigCause(ParseIntError { kind: InvalidDigit }))");
should_eq!(config: "h" as u8 = "Ok(0)");
should_eq!(config: "i" as u8 = "Err(ConfigCause(ParseIntError { kind: InvalidDigit }))");
should_eq!(config: "j" as u8 = "Ok(0)");
should_eq!(config: "k" as u8 = "Err(ConfigCause(ParseIntError { kind: InvalidDigit }))");
should_eq!(config: "l" as u8 = "Ok(0)");
should_eq!(config: "m" as u8 = "Err(ConfigCause(ParseIntError { kind: InvalidDigit }))");
should_eq!(config: "n" as u8 = "Err(ConfigParseError(\"n\", \"$\"))");
should_eq!(config: "o" as u8 = "Err(ConfigParseError(\"o\", \"\\\\\"))");
should_eq!(config: "p" as u8 = "Err(ConfigParseError(\"p\", \"}\"))");
should_eq!(config: "q" as u8 = "Err(ConfigParseError(\"q\", \"${\"))");
}
#[test]
fn get_test() {
let config = build_config()
.register_kv("k1")
.set("key[0]", "xx")
.finish()
.unwrap()
.register_kv("k2")
.set("key[5]", "xx")
.finish()
.unwrap()
.register_kv("k3")
.set("key[3]", "xx")
.finish()
.unwrap();
assert_eq!(1, config.get_or("a1", 1).unwrap());
let v = config.get::<Vec<Option<String>>>("key").unwrap();
assert_eq!(
vec![
Some("xx".to_string()),
None,
None,
Some("xx".to_string()),
None,
Some("xx".to_string())
],
v
);
}
#[test]
fn predefined_test() {
let _config = Configuration::with_predefined().unwrap();
let _conf2 = Configuration::with_predefined_builder().init().unwrap();
println!("Total count = {}", _conf2.source.value.len());
for v in _config.source_names() {
println!("{}", v);
}
assert_eq!(_conf2.source.value.len(), _config.source.value.len());
init_cargo_env!();
let _conf3 = Configuration::with_predefined_builder()
.set("key", "value")
.set_prefix_env("XXX")
.set_cargo_env(init_cargo_env())
.init()
.unwrap();
let _conf3 = Configuration::with_predefined_builder()
.set_dir("")
.set_name("app")
.set_profile("dev")
.set_prefix_env("XXX")
.set_cargo_env(init_cargo_env())
.init()
.unwrap();
match _config.register_file("/conf/no_extension", false) {
Err(ConfigError::ConfigFileNotSupported(_)) => {}
_ => assert_eq!(true, false),
}
match _conf2.register_file("/conf/app.not_exist", false) {
Err(ConfigError::ConfigFileNotSupported(_)) => {}
_ => assert_eq!(true, false),
}
}
}