use crate::engine::{Engine, Folded, Layer};
use crate::error::Error;
use crate::value::Value;
use std::collections::BTreeMap;
#[derive(Debug)]
pub(crate) struct ConfigRs;
impl Engine for ConfigRs {
fn name(&self) -> &str {
"config-rs"
}
fn fold(&self, layers: &[Layer<'_>]) -> Result<Folded, Error> {
let names = Names::of(layers);
let mut builder = config_rs::Config::builder();
for layer in layers {
builder = builder.add_source(Contribution {
tag: layer.tag,
values: names.encode(layer.values),
});
}
let merged = builder
.build()
.and_then(|config| config_rs::Source::collect(&config))
.map_err(|error| super::error::unfolded(&error))?;
let mut values = BTreeMap::new();
let mut tags = BTreeMap::new();
for (token, value) in merged {
let key = names.decode(&token);
let mut path = vec![key.clone()];
values.insert(key, unpack(&value, &mut path, &mut tags));
}
Ok(Folded {
values: Value::Table(values),
tags,
})
}
}
struct Names(Vec<String>);
impl Names {
fn of(layers: &[Layer<'_>]) -> Self {
let mut names: Vec<String> = layers
.iter()
.filter_map(|layer| match layer.values {
Value::Table(table) => Some(table.keys().cloned()),
_ => None,
})
.flatten()
.collect();
names.sort_unstable();
names.dedup();
Self(names)
}
fn encode(&self, values: &Value) -> BTreeMap<String, Value> {
let Value::Table(table) = values else {
return BTreeMap::new();
};
table
.iter()
.map(|(key, value)| {
let token = self
.0
.binary_search(key)
.map_or_else(|_| key.clone(), |index| format!("key{index}"));
(token, value.clone())
})
.collect()
}
fn decode(&self, token: &str) -> String {
token
.strip_prefix("key")
.and_then(|index| index.parse::<usize>().ok())
.and_then(|index| self.0.get(index).cloned())
.unwrap_or_else(|| token.to_owned())
}
}
#[derive(Debug, Clone)]
struct Contribution {
tag: usize,
values: BTreeMap<String, Value>,
}
impl config_rs::Source for Contribution {
fn clone_into_box(&self) -> Box<dyn config_rs::Source + Send + Sync> {
Box::new(self.clone())
}
fn collect(&self) -> Result<config_rs::Map<String, config_rs::Value>, config_rs::ConfigError> {
let tag = self.tag.to_string();
Ok(self
.values
.iter()
.map(|(key, value)| (key.clone(), pack(value, &tag)))
.collect())
}
}
fn pack(value: &Value, tag: &String) -> config_rs::Value {
let tagged = !matches!(value, Value::Table(table) if !table.is_empty());
let kind = match value {
Value::Null => config_rs::ValueKind::Nil,
Value::Bool(boolean) => config_rs::ValueKind::Boolean(*boolean),
Value::Integer(number) => i64::try_from(*number).map_or_else(
|_| {
u64::try_from(*number).map_or(
config_rs::ValueKind::I128(*number),
config_rs::ValueKind::U64,
)
},
config_rs::ValueKind::I64,
),
Value::Float(number) => config_rs::ValueKind::Float(*number),
Value::String(text) => config_rs::ValueKind::String(text.clone()),
Value::Array(values) => {
config_rs::ValueKind::Array(values.iter().map(|value| pack(value, tag)).collect())
}
Value::Table(table) => config_rs::ValueKind::Table(
table
.iter()
.map(|(key, value)| (key.clone(), pack(value, tag)))
.collect(),
),
};
if tagged {
config_rs::Value::new(Some(tag), kind)
} else {
config_rs::Value::new(None, kind)
}
}
fn unpack(
value: &config_rs::Value,
path: &mut Vec<String>,
tags: &mut BTreeMap<String, usize>,
) -> Value {
if let config_rs::ValueKind::Table(table) = &value.kind {
if table.is_empty() {
remember(value.origin(), path, tags);
return Value::Table(BTreeMap::new());
}
let mut values = BTreeMap::new();
for (key, value) in table {
path.push(key.clone());
values.insert(key.clone(), unpack(value, path, tags));
path.pop();
}
return Value::Table(values);
}
remember(value.origin(), path, tags);
match &value.kind {
config_rs::ValueKind::Array(values) => Value::Array(
values
.iter()
.map(|value| unpack(value, path, tags))
.collect(),
),
config_rs::ValueKind::Table(_) => unreachable!("a table was handled above"),
_ => super::from_config_rs(value),
}
}
fn remember(origin: Option<&str>, path: &[String], tags: &mut BTreeMap<String, usize>) {
if let Some(tag) = origin.and_then(|origin| origin.parse().ok()) {
tags.insert(path.join("."), tag);
}
}