use std::collections::BTreeMap;
use std::sync::Mutex;
use crate::error::Error;
pub(crate) const SECTION: &str = "::";
pub(crate) fn split_section(path: &str) -> (Option<&str>, &str) {
match path.split_once(SECTION) {
Some((section, rest)) => (Some(section), rest),
None => (None, path),
}
}
fn check_old_path(from: &str) -> Result<(), Error> {
let (section, path) = split_section(from);
if let Some(section) = section {
if section.is_empty() || section.contains('.') || path.contains(SECTION) {
return Err(Error::new(
crate::ErrorKind::Type,
format!(
"`{from}` is not a usable old key path: `{SECTION}` names one \
top-level section, as in `db{SECTION}pool.size`"
),
));
}
}
crate::layer::check_path(path)
}
#[derive(Debug, Default)]
pub struct Aliases {
entries: Mutex<BTreeMap<String, String>>,
}
impl Aliases {
#[must_use]
pub const fn new() -> Self {
Self {
entries: Mutex::new(BTreeMap::new()),
}
}
pub fn add(&self, from: &str, to: &str) -> Result<(), Error> {
check_old_path(from)?;
if to.contains(SECTION) {
return Err(Error::new(
crate::ErrorKind::Type,
format!(
"`{to}` names another section, and an alias's new path is \
always in this configuration's own section; a section \
qualifier belongs on the old path, as in \
`alias(\"{to}\", ..)`"
),
));
}
crate::layer::check_path(to)?;
if from == to {
return Err(Error::new(
crate::ErrorKind::Type,
format!("`{from}` cannot be an alias for itself"),
));
}
{
let mut entries = self.lock();
let mut cursor = to.to_owned();
let mut hops = 0usize;
while let Some(next) = entries.get(&cursor) {
if next == from || hops > entries.len() {
return Err(Error::new(
crate::ErrorKind::Type,
format!(
"`{from}` -> `{to}` closes an alias cycle; renames \
must form a chain, not a loop"
),
));
}
cursor = next.clone();
hops += 1;
}
entries.insert(from.to_owned(), to.to_owned());
}
Ok(())
}
pub fn clear(&self) {
self.lock().clear();
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.lock().is_empty()
}
#[must_use]
pub fn pairs(&self) -> Vec<(String, String)> {
self.lock()
.iter()
.map(|(from, to)| (from.clone(), to.clone()))
.collect()
}
#[must_use]
pub fn known_keys(&self) -> Vec<String> {
self.lock()
.keys()
.filter(|path| !path.contains(SECTION))
.filter_map(|path| path.split('.').next().map(str::to_owned))
.collect()
}
fn lock(&self) -> std::sync::MutexGuard<'_, BTreeMap<String, String>> {
self.entries
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_path_that_names_nothing_is_refused() {
let aliases = Aliases::new();
assert!(aliases.add("", "pool.max_size").is_err());
assert!(aliases.add("pool..size", "pool.max_size").is_err());
assert!(aliases.add("pool.size", "").is_err());
}
#[test]
fn an_alias_to_itself_is_refused() {
let aliases = Aliases::new();
let error = aliases.add("pool.size", "pool.size").unwrap_err();
assert!(error.to_string().contains("itself"), "{error}");
}
#[test]
fn the_old_paths_top_level_key_counts_as_known() {
let aliases = Aliases::new();
aliases.add("legacy.size", "pool.max_size").unwrap();
aliases.add("host", "hostname").unwrap();
let known = aliases.known_keys();
assert!(known.contains(&"legacy".to_owned()));
assert!(known.contains(&"host".to_owned()));
}
#[test]
fn a_section_qualified_old_path_is_accepted() {
let aliases = Aliases::new();
aliases.add("db::timeout", "timeout").unwrap();
assert_eq!(
aliases.pairs(),
vec![("db::timeout".to_owned(), "timeout".to_owned())]
);
}
#[test]
fn only_the_old_path_may_name_a_section() {
let aliases = Aliases::new();
let error = aliases.add("timeout", "server::timeout").unwrap_err();
assert!(error.to_string().contains("own section"), "{error}");
}
#[test]
fn a_qualifier_names_one_top_level_section() {
let aliases = Aliases::new();
assert!(aliases.add("::timeout", "timeout").is_err(), "no section");
assert!(aliases.add("a::b::c", "timeout").is_err(), "two of them");
assert!(aliases.add("a.b::c", "timeout").is_err(), "not top level");
assert!(aliases.add("db::", "timeout").is_err(), "no path");
}
#[test]
fn a_cross_section_alias_cannot_be_chained_into() {
let aliases = Aliases::new();
aliases.add("db::timeout", "timeout").unwrap();
aliases.add("timeout", "deadline").unwrap();
let targets: Vec<String> = aliases.pairs().into_iter().map(|(_, to)| to).collect();
assert!(
targets.iter().all(|to| !to.contains(SECTION)),
"nothing points at a section-qualified path: {targets:?}"
);
}
#[test]
fn a_foreign_sections_key_is_not_known_in_this_one() {
let aliases = Aliases::new();
aliases.add("db::timeout", "timeout").unwrap();
assert!(
aliases.known_keys().is_empty(),
"`db` is another section's key, not a legitimate key here"
);
}
#[test]
fn aliasing_the_same_path_twice_replaces_rather_than_layers() {
let aliases = Aliases::new();
aliases.add("old", "first").unwrap();
aliases.add("old", "second").unwrap();
assert_eq!(
aliases.pairs(),
vec![("old".to_owned(), "second".to_owned())]
);
}
}