use std::collections::{HashMap, HashSet};
use thiserror::Error;
use crate::Configuration;
use crate::configuration_provider::ConfigurationProvider;
use crate::value::Value;
pub struct ConfigurationBuilder {
value: Value,
providers: HashMap<String, Value>,
}
impl ConfigurationBuilder {
pub(crate) fn new(provider: impl ConfigurationProvider) -> Self {
ConfigurationBuilder {
value: provider.provide(),
providers: HashMap::new(),
}
}
pub fn set_provider(&mut self, key: impl ToString, provider: impl ConfigurationProvider) {
self.providers.insert(key.to_string(), provider.provide());
}
pub fn with_provider(mut self, key: impl ToString, provider: impl ConfigurationProvider) -> Self {
self.set_provider(key, provider);
self
}
pub fn build(self) -> Result<Configuration, BuildConfigurationError> {
let mut hashset: HashSet<String> = HashSet::new();
let resolve_ref_ctx = ResolveRefCtx {
key: KeyInfo::None,
hashset: &mut hashset,
};
Ok(Configuration::new(resolver_refs(
self.value,
&self.providers,
resolve_ref_ctx,
)?))
}
}
#[derive(Debug, Error)]
pub enum BuildConfigurationError {
#[error("Circular reference: {}", .refs.join(" -> "))]
CircularReference { refs: Vec<String> },
#[error("Invalid reference [reference = `{0}`]")]
InvalidReference(String),
#[error("Reference does not exists [reference = `{0}`]")]
ReferenceDoesNotExists(String),
#[error("Provider does not exists [provider = `{0}`]")]
ProviderDoesNotExists(String),
}
fn resolver_refs(
value: Value,
providers: &HashMap<String, Value>,
mut ctx: ResolveRefCtx,
) -> Result<Value, BuildConfigurationError> {
Ok(match value {
Value::String(string) => {
if let Some((provider_key, key)) = is_ref(&string)? {
match providers.get(&provider_key) {
None => return Err(BuildConfigurationError::ProviderDoesNotExists(provider_key)),
Some(provider) => match provider.get(&key) {
None => return Err(BuildConfigurationError::ReferenceDoesNotExists(key)),
Some(value) => resolver_refs(
value.clone(),
providers,
ctx.enter_ref(&provider_key, &key)?,
)?,
},
}
} else {
Value::String(string)
}
}
Value::Bool(bool) => Value::Bool(bool),
Value::Number(number) => Value::Number(number),
Value::None => Value::None,
Value::Map(map) => Value::Map(
map.into_iter()
.map(|(k, v)| {
let value = resolver_refs(v, providers, ctx.enter_key(&k))?;
Ok((k, value))
})
.collect::<Result<HashMap<_, _>, BuildConfigurationError>>()?,
),
Value::Array(array) => Value::Array(
array
.into_iter()
.enumerate()
.map(|(i, v)| {
resolver_refs(v, providers, ctx.enter_key(&i.to_string()))
})
.collect::<Result<Vec<_>, BuildConfigurationError>>()?,
),
})
}
fn is_ref(str: &str) -> Result<Option<(String, String)>, BuildConfigurationError> {
let Some(inner) = str.strip_prefix("{{") else { return Ok(None); };
let Some(inner) = inner.strip_suffix("}}") else { return Ok(None); };
let Some((provider, key)) = inner.split_once('.') else {
return Err(BuildConfigurationError::ReferenceDoesNotExists(inner.to_string()));
};
Ok(Some((provider.trim().to_string(), key.trim().to_string())))
}
struct ResolveRefCtx<'a> {
key: KeyInfo<'a>,
hashset: &'a mut HashSet<String>,
}
enum KeyInfo<'a> {
None,
Key {
prev: &'a KeyInfo<'a>,
key: &'a str,
},
Ref {
prev: &'a KeyInfo<'a>,
ref_key: String,
provider_key: &'a str,
key: &'a str,
},
}
impl<'a> ResolveRefCtx<'a> {
pub fn enter_key<'b>(&'b mut self, key: &'b str) -> ResolveRefCtx<'b> {
ResolveRefCtx {
key: KeyInfo::Key {
prev: &self.key,
key,
},
hashset: self.hashset,
}
}
pub fn enter_ref<'b>(
&'b mut self,
provider_key: &'b str,
key: &'b str,
) -> Result<ResolveRefCtx<'b>, BuildConfigurationError> {
let mut current_key: Vec<&str> = Vec::new();
let mut current = &self.key;
loop {
match current {
KeyInfo::None => break,
KeyInfo::Key {
prev: prev_ref,
key,
} => {
current_key.push(key);
current = prev_ref;
}
KeyInfo::Ref {
provider_key, key, ..
} => {
current_key.push(provider_key);
current_key.push(".");
current_key.push(key);
break;
}
}
}
current_key.reverse();
let current_key = current_key.join(".");
if !self.hashset.insert(current_key.clone()) {
let refs = self.collect_keys();
return Err(BuildConfigurationError::CircularReference { refs });
};
Ok(ResolveRefCtx {
key: KeyInfo::Ref {
prev: &self.key,
ref_key: current_key,
provider_key,
key,
},
hashset: self.hashset,
})
}
fn collect_keys(&self) -> Vec<String> {
let mut out = Vec::new();
let mut current: Vec<&str> = Vec::new();
let mut key_ref = &self.key;
loop {
match key_ref {
KeyInfo::None => {
current.reverse();
out.push(current.join("."));
out.reverse();
return out;
}
KeyInfo::Key {
key,
prev: prev_ref,
} => {
current.push(key);
key_ref = prev_ref;
}
KeyInfo::Ref {
prev: prev_ref,
key,
provider_key,
..
} => {
current.push(key);
current.push(provider_key);
current.reverse();
out.push(current.join("."));
current.clear();
key_ref = prev_ref;
}
}
}
}
}
impl<'a> Drop for ResolveRefCtx<'a> {
fn drop(&mut self) {
if let KeyInfo::Ref { ref_key, .. } = &self.key {
self.hashset.remove(ref_key);
}
}
}