use std::collections::{BTreeMap, BTreeSet};
use toml_edit::{Decor, DocumentMut, Item, Table, Value};
use crate::output::{Output, OutputFormat};
use crate::{ConfigErr, ConfigResult, ConfigType, ConfigValue};
type ConfigTable = BTreeMap<String, ConfigItem>;
#[derive(Debug, Clone)]
pub struct ConfigItem {
table_name: String,
key: String,
value: ConfigValue,
comments: String,
}
impl ConfigItem {
fn new(table_name: &str, table: &Table, key: &str, value: &Value) -> ConfigResult<Self> {
let inner = || {
let item = table.key(key).unwrap();
let comments = prefix_comments(item.leaf_decor())
.unwrap_or_default()
.to_string();
let suffix = suffix_comments(value.decor()).unwrap_or_default().trim();
let value = if !suffix.is_empty() {
let ty_str = suffix.trim_start_matches('#');
let ty = ConfigType::new(ty_str)?;
ConfigValue::from_raw_value_type(value, ty)?
} else {
ConfigValue::from_raw_value(value)?
};
Ok(Self {
table_name: table_name.into(),
key: key.into(),
value,
comments,
})
};
let res = inner();
if let Err(e) = &res {
eprintln!("Parsing error at key `{}`: {:?}", key, e);
}
res
}
fn new_global(table: &Table, key: &str, value: &Value) -> ConfigResult<Self> {
Self::new(Config::GLOBAL_TABLE_NAME, table, key, value)
}
pub fn item_name(&self) -> String {
if self.table_name == Config::GLOBAL_TABLE_NAME {
self.key.clone()
} else {
format!("{}.{}", self.table_name, self.key)
}
}
pub fn table_name(&self) -> &str {
&self.table_name
}
pub fn key(&self) -> &str {
&self.key
}
pub fn value(&self) -> &ConfigValue {
&self.value
}
pub fn comments(&self) -> &str {
&self.comments
}
pub fn value_mut(&mut self) -> &mut ConfigValue {
&mut self.value
}
}
#[derive(Default, Debug)]
pub struct Config {
global: ConfigTable,
tables: BTreeMap<String, ConfigTable>,
table_comments: BTreeMap<String, String>,
}
impl Config {
pub const GLOBAL_TABLE_NAME: &'static str = "$GLOBAL";
pub fn new() -> Self {
Self {
global: ConfigTable::new(),
tables: BTreeMap::new(),
table_comments: BTreeMap::new(),
}
}
pub fn is_empty(&self) -> bool {
self.global.is_empty() && self.tables.is_empty()
}
fn new_table(&mut self, name: &str, comments: &str) -> ConfigResult<&mut ConfigTable> {
if name == Self::GLOBAL_TABLE_NAME {
return Err(ConfigErr::Other(format!(
"Table name `{}` is reserved",
Self::GLOBAL_TABLE_NAME
)));
}
if self.tables.contains_key(name) {
return Err(ConfigErr::Other(format!("Duplicate table name `{}`", name)));
}
self.tables.insert(name.into(), ConfigTable::new());
self.table_comments.insert(name.into(), comments.into());
Ok(self.tables.get_mut(name).unwrap())
}
pub fn global_table(&self) -> &BTreeMap<String, ConfigItem> {
&self.global
}
pub fn table_at(&self, name: &str) -> Option<&BTreeMap<String, ConfigItem>> {
if name == Self::GLOBAL_TABLE_NAME {
Some(&self.global)
} else {
self.tables.get(name)
}
}
pub fn table_at_mut(&mut self, name: &str) -> Option<&mut BTreeMap<String, ConfigItem>> {
if name == Self::GLOBAL_TABLE_NAME {
Some(&mut self.global)
} else {
self.tables.get_mut(name)
}
}
pub fn config_at(&self, table: &str, key: &str) -> Option<&ConfigItem> {
self.table_at(table).and_then(|t| t.get(key))
}
pub fn config_at_mut(&mut self, table: &str, key: &str) -> Option<&mut ConfigItem> {
self.table_at_mut(table).and_then(|t| t.get_mut(key))
}
pub fn table_comments_at(&self, name: &str) -> Option<&str> {
self.table_comments.get(name).map(|s| s.as_str())
}
pub fn table_iter(&self) -> impl Iterator<Item = (&str, &ConfigTable, &str)> {
let global_iter = [(Self::GLOBAL_TABLE_NAME, &self.global, "")].into_iter();
let other_iter = self.tables.iter().map(|(name, configs)| {
(
name.as_str(),
configs,
self.table_comments.get(name).unwrap().as_str(),
)
});
global_iter.chain(other_iter)
}
pub fn iter(&self) -> impl Iterator<Item = &ConfigItem> {
self.table_iter().flat_map(|(_, c, _)| c.values())
}
}
impl Config {
pub fn from_toml(toml: &str) -> ConfigResult<Self> {
let doc = toml.parse::<DocumentMut>()?;
let table = doc.as_table();
let mut result = Self::new();
for (key, item) in table.iter() {
match item {
Item::Value(val) => {
result
.global
.insert(key.into(), ConfigItem::new_global(table, key, val)?);
}
Item::Table(table) => {
let table_name = key;
let comments = prefix_comments(table.decor());
let configs = result.new_table(key, comments.unwrap_or_default())?;
for (key, item) in table.iter() {
if let Item::Value(val) = item {
configs
.insert(key.into(), ConfigItem::new(table_name, table, key, val)?);
} else {
return Err(ConfigErr::InvalidValue);
}
}
}
Item::None => {}
_ => {
return Err(ConfigErr::Other(format!(
"Object array `[[{}]]` is not supported",
key
)))
}
}
}
Ok(result)
}
pub fn dump(&self, fmt: OutputFormat) -> ConfigResult<String> {
let mut output = Output::new(fmt);
for (name, table, comments) in self.table_iter() {
if name != Self::GLOBAL_TABLE_NAME {
output.table_begin(name, comments);
}
for (key, item) in table.iter() {
if let Err(e) = output.write_item(item) {
eprintln!("Dump config `{}` failed: {:?}", key, e);
}
}
if name != Self::GLOBAL_TABLE_NAME {
output.table_end();
}
}
Ok(output.result().into())
}
pub fn dump_toml(&self) -> ConfigResult<String> {
self.dump(OutputFormat::Toml)
}
pub fn dump_rs(&self) -> ConfigResult<String> {
self.dump(OutputFormat::Rust)
}
pub fn merge(&mut self, other: &Self) -> ConfigResult<()> {
for (name, other_table, table_comments) in other.table_iter() {
let self_table = if let Some(table) = self.table_at_mut(name) {
table
} else {
self.new_table(name, table_comments)?
};
for (key, item) in other_table.iter() {
if self_table.contains_key(key) {
return Err(ConfigErr::Other(format!("Duplicate key `{}`", key)));
} else {
self_table.insert(key.into(), item.clone());
}
}
}
Ok(())
}
pub fn update(&mut self, other: &Self) -> ConfigResult<(Vec<ConfigItem>, Vec<ConfigItem>)> {
let mut touched = BTreeSet::new(); let mut extra = Vec::new();
for other_item in other.iter() {
let table_name = other_item.table_name.clone();
let key = other_item.key.clone();
let self_table = if let Some(table) = self.table_at_mut(&table_name) {
table
} else {
extra.push(other_item.clone());
continue;
};
if let Some(self_item) = self_table.get_mut(&key) {
self_item.value.update(other_item.value.clone())?;
touched.insert(self_item.item_name());
} else {
extra.push(other_item.clone());
}
}
let untouched = self
.iter()
.filter(|item| !touched.contains(&item.item_name()))
.cloned()
.collect::<Vec<_>>();
Ok((untouched, extra))
}
}
fn prefix_comments(decor: &Decor) -> Option<&str> {
decor.prefix().and_then(|s| s.as_str())
}
fn suffix_comments(decor: &Decor) -> Option<&str> {
decor.suffix().and_then(|s| s.as_str())
}