use dynamic_config::{Error, Fetched, Format, Value};
pub const MOST_KEYS: usize = 512;
const MOST_REPORTED_PATHS: usize = 8;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Overlap {
LaterWins,
Refused,
}
pub fn merged(
documents: &[(String, String)],
format: Format,
overlap: Overlap,
described: &str,
) -> Result<Fetched, Error> {
match documents {
[] => Err(Error::remote(format!(
"{described}: no key held a value, so there is nothing to load"
))),
[(_, only)] => Ok(Fetched::new(only.clone(), format)),
_ => Ok(Fetched::new(
folded(documents, format, overlap, described)?
.render(format)
.map_err(|error| {
Error::remote(format!(
"{described}: the merged document cannot be written back as {format:?}: {error}"
))
})?,
format,
)),
}
}
fn folded(
documents: &[(String, String)],
format: Format,
overlap: Overlap,
described: &str,
) -> Result<Value, Error> {
let mut parsed: Vec<(&str, Value)> = Vec::with_capacity(documents.len());
for (key, text) in documents {
let value = Value::parse(text, format).map_err(|error| {
Error::remote(format!(
"{described}: `{key}` is not a {format:?} document: {error}"
))
})?;
parsed.push((key.as_str(), value));
}
let mut folded = parsed[0].1.clone();
for (key, value) in &parsed[1..] {
if overlap == Overlap::Refused {
let clashes = folded.overlapping_paths(value);
if !clashes.is_empty() {
return Err(collision(&parsed, key, &clashes, described));
}
}
folded.merge(value.clone());
}
Ok(folded)
}
fn collision(parsed: &[(&str, Value)], key: &str, clashes: &[String], described: &str) -> Error {
let before = parsed
.iter()
.position(|(name, _)| *name == key)
.unwrap_or(parsed.len());
let earlier = clashes
.first()
.and_then(|path| {
parsed[..before]
.iter()
.rev()
.find(|(_, value)| value.get(path).is_some())
})
.map_or("an earlier key", |(name, _)| *name);
let named: Vec<&str> = clashes
.iter()
.take(MOST_REPORTED_PATHS)
.map(String::as_str)
.collect();
let more = clashes.len().saturating_sub(named.len());
let and_more = if more == 0 {
String::new()
} else {
format!(" (and {more} more)")
};
Error::remote(format!(
"{described}: `{earlier}` and `{key}` both supply {}{and_more}; keys read \
as a prefix are sections that must not overlap, and the order a server \
lists them in is not a precedence — name the keys instead if one is \
meant to win",
named.join(", ")
))
}
pub fn agreed_format(keys: &[String]) -> Result<Option<Format>, String> {
let mut agreed: Option<(&str, Format)> = None;
for key in keys {
let Some(format) = Format::from_key(key) else {
continue;
};
match agreed {
Some((named, first)) if first != format => {
return Err(format!(
"`{named}` names {first:?} and `{key}` names {format:?}; one source \
reads one format — call `with_format` to settle it, or install one \
source per format"
));
}
Some(_) => {}
None => agreed = Some((key, format)),
}
}
Ok(agreed.map(|(_, format)| format))
}
pub fn within_key_budget(matched: usize, described: &str) -> Result<(), Error> {
if matched <= MOST_KEYS {
return Ok(());
}
Err(Error::remote(format!(
"{described}: the prefix matches {matched} keys, above the {MOST_KEYS} \
one document is folded from; narrow the prefix"
)))
}
pub fn under_prefix(key: &str, prefix: &str, described: &str) -> Result<(), Error> {
if key.starts_with(prefix) {
return Ok(());
}
Err(Error::remote(format!(
"{described}: the store answered with `{key}`, which is not under the \
prefix that was asked for"
)))
}
#[cfg(test)]
mod tests {
use super::*;
fn documents(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|(key, text)| ((*key).to_owned(), (*text).to_owned()))
.collect()
}
#[test]
fn a_named_list_merges_in_order_and_the_later_key_wins() {
let documents = documents(&[
("myapp/base", r#"{"db": {"host": "a", "port": 1}}"#),
("myapp/local", r#"{"db": {"port": 2}}"#),
]);
let fetched = merged(&documents, Format::Json, Overlap::LaterWins, "store")
.expect("two documents merge");
let tree = Value::parse(&fetched.text, Format::Json).expect("the result is a document");
assert_eq!(tree.get("db.host"), Some(&Value::String("a".to_owned())));
assert_eq!(tree.get("db.port"), Some(&Value::Integer(2)));
}
#[test]
fn disjoint_sections_fold_into_one_document_under_either_rule() {
let documents = documents(&[
("myapp/db", r#"{"db": {"host": "a"}}"#),
("myapp/server", r#"{"server": {"port": 8080}}"#),
]);
for overlap in [Overlap::LaterWins, Overlap::Refused] {
let fetched =
merged(&documents, Format::Json, overlap, "store").expect("nothing overlaps");
let tree = Value::parse(&fetched.text, Format::Json).expect("the result is a document");
assert_eq!(tree.get("db.host"), Some(&Value::String("a".to_owned())));
assert_eq!(tree.get("server.port"), Some(&Value::Integer(8080)));
}
}
#[test]
fn a_prefix_collision_names_both_keys_and_the_path() {
let documents = documents(&[
("myapp/db", r#"{"db": {"host": "a"}}"#),
("myapp/server", r#"{"server": {"port": 1}}"#),
("myapp/extra", r#"{"db": {"host": "b"}}"#),
]);
let error = merged(&documents, Format::Json, Overlap::Refused, "store")
.expect_err("two keys supply db.host");
let printed = error.to_string();
assert!(printed.contains("myapp/db"), "{printed}");
assert!(printed.contains("myapp/extra"), "{printed}");
assert!(printed.contains("db.host"), "{printed}");
assert!(!printed.contains("myapp/server"), "{printed}");
}
#[test]
fn a_collision_report_names_paths_and_never_values() {
let documents = documents(&[
("myapp/db", r#"{"db": {"password": "hunter2-left"}}"#),
("myapp/extra", r#"{"db": {"password": "hunter2-right"}}"#),
]);
let error = merged(&documents, Format::Json, Overlap::Refused, "store")
.expect_err("both keys supply db.password");
let printed = format!("{error} {error:?}");
assert!(printed.contains("db.password"), "{printed}");
assert!(!printed.contains("hunter2"), "{printed}");
}
#[test]
fn a_document_that_does_not_parse_names_its_key_and_not_its_contents() {
let documents = documents(&[
("myapp/db", r#"{"db": {"host": "a"}}"#),
("myapp/broken", r#"{"password": "hunter2"#),
]);
let error = merged(&documents, Format::Json, Overlap::LaterWins, "store")
.expect_err("the second document is truncated");
let printed = format!("{error} {error:?}");
assert!(printed.contains("myapp/broken"), "{printed}");
assert!(!printed.contains("hunter2"), "{printed}");
}
#[test]
fn one_document_is_handed_over_exactly_as_it_was_stored() {
let stored = "{\n \"zebra\": 1,\n \"apple\": 2\n}\n";
let fetched = merged(
&documents(&[("myapp/db", stored)]),
Format::Json,
Overlap::Refused,
"store",
)
.expect("one document needs no merge");
assert_eq!(fetched.text, stored);
}
#[test]
fn no_keys_at_all_is_a_failure_rather_than_an_empty_document() {
let error = merged(&[], Format::Json, Overlap::Refused, "store")
.expect_err("an empty set is not a configuration");
assert!(error.to_string().contains("nothing to load"), "{error}");
}
#[test]
fn a_prefix_matching_more_keys_than_the_budget_is_refused() {
within_key_budget(MOST_KEYS, "store").expect("the budget itself is allowed");
let error = within_key_budget(MOST_KEYS + 1, "store").expect_err("one too many");
assert!(error.to_string().contains("narrow the prefix"), "{error}");
}
#[test]
fn keys_that_name_one_format_agree_and_keys_that_name_none_defer() {
let keys = ["a/db.json".to_owned(), "a/server.json".to_owned()];
assert_eq!(agreed_format(&keys), Ok(Some(Format::Json)));
let keys = ["a/db.json".to_owned(), "a/server".to_owned()];
assert_eq!(agreed_format(&keys), Ok(Some(Format::Json)));
assert_eq!(agreed_format(&["a/db".to_owned()]), Ok(None));
assert_eq!(agreed_format(&[]), Ok(None));
}
#[test]
fn keys_naming_two_formats_name_both_keys_rather_than_guessing() {
let keys = ["a/db.json".to_owned(), "a/server.toml".to_owned()];
let complaint = agreed_format(&keys).expect_err("json and toml cannot both be it");
assert!(complaint.contains("a/db.json"), "{complaint}");
assert!(complaint.contains("a/server.toml"), "{complaint}");
assert!(complaint.contains("with_format"), "{complaint}");
}
#[test]
fn a_key_outside_the_prefix_is_refused() {
under_prefix("myapp/db", "myapp/", "store").expect("this one is under it");
let error =
under_prefix("other/db", "myapp/", "store").expect_err("that one is not under it");
assert!(error.to_string().contains("other/db"), "{error}");
}
#[test]
fn a_later_key_replaces_a_list_rather_than_appending_to_it() {
let documents = documents(&[
("myapp/base", r#"{"db": {"hosts": ["a", "b"]}}"#),
("myapp/local", r#"{"db": {"hosts": ["c"]}}"#),
]);
let fetched = merged(&documents, Format::Json, Overlap::LaterWins, "store")
.expect("two documents merge");
let tree = Value::parse(&fetched.text, Format::Json).expect("the result is a document");
assert_eq!(
tree.get("db.hosts"),
Some(&Value::Array(vec![Value::String("c".to_owned())]))
);
}
}