use std::{collections::HashMap, path::PathBuf};
use crate::{ConfigError, tree::path::ConfigPath};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergePolicy {
RejectOverlap,
Override,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Origin {
MainConfig(PathBuf),
SystemConfig(PathBuf),
BuiltInDefault,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OriginId(usize);
impl OriginId {
pub const BUILT_IN_DEFAULT: Self = Self(0);
}
#[derive(Debug)]
pub struct ProvenanceTracker {
origins: HashMap<OriginId, Origin>,
}
impl ProvenanceTracker {
pub fn new() -> Self {
Self {
origins: HashMap::new(),
}
}
pub fn track(&mut self, origin: Origin) -> OriginId {
if origin == Origin::BuiltInDefault {
return OriginId::BUILT_IN_DEFAULT;
}
if let Some((id, _)) = self.origins.iter().find(|(_, o)| *o == &origin) {
return *id;
}
let id = OriginId(self.origins.len() + 1);
self.origins.insert(id, origin);
id
}
pub fn get_origin(&self, id: OriginId) -> Option<&Origin> {
if id == OriginId::BUILT_IN_DEFAULT {
return Some(&Origin::BuiltInDefault);
}
self.origins.get(&id)
}
}
impl Default for ProvenanceTracker {
fn default() -> Self {
Self::new()
}
}
pub struct MergeContext<'a> {
#[doc(hidden)]
pub policy: MergePolicy,
pub path: ConfigPath,
provenance: &'a ProvenanceTracker,
}
impl<'a> MergeContext<'a> {
pub(crate) fn new(policy: MergePolicy, provenance: &'a ProvenanceTracker) -> Self {
Self {
policy,
path: ConfigPath::root(),
provenance,
}
}
pub fn at<R>(&mut self, field: &'static str, f: impl FnOnce(&mut Self) -> R) -> R {
self.path.push_field(field);
let result = f(self);
self.path.pop();
result
}
pub(crate) fn origin(&self, id: Option<OriginId>) -> Option<Origin> {
id.and_then(|id| self.provenance.get_origin(id)).cloned()
}
}
pub trait Merge {
fn merge(&mut self, incoming: Self, context: &mut MergeContext<'_>) -> Result<(), ConfigError>;
}
#[cfg(test)]
mod tests {
use crate::{
Setting,
fixture::{PartialPool, PartialServer, PartialSource},
};
use super::*;
#[test]
fn the_built_in_default_origin_is_reserved() {
let mut tracker = ProvenanceTracker::new();
assert_eq!(
tracker.get_origin(OriginId::BUILT_IN_DEFAULT),
Some(&Origin::BuiltInDefault)
);
assert_eq!(
tracker.track(Origin::BuiltInDefault),
OriginId::BUILT_IN_DEFAULT
);
let first = tracker.track(Origin::MainConfig("/etc/ntp.toml".into()));
let second = tracker.track(Origin::SystemConfig("/etc/ntp.d/a.toml".into()));
assert_ne!(first, OriginId::BUILT_IN_DEFAULT);
assert_ne!(second, OriginId::BUILT_IN_DEFAULT);
assert_ne!(first, second);
assert_eq!(
tracker.get_origin(first),
Some(&Origin::MainConfig("/etc/ntp.toml".into()))
);
}
#[test]
fn merge_of_enum_merges_identical() {
let mut left = PartialSource::Server(PartialServer {
address: Setting::Unset,
version: Setting::Set {
value: 5,
origin: None,
},
});
let right = PartialSource::Server(PartialServer {
address: Setting::Set {
value: "hello".into(),
origin: None,
},
version: Setting::Unset,
});
left.merge(
right,
&mut MergeContext {
policy: MergePolicy::RejectOverlap,
path: ConfigPath::root(),
provenance: &ProvenanceTracker::new(),
},
)
.unwrap();
assert_eq!(
left,
PartialSource::Server(PartialServer {
address: Setting::Set {
value: "hello".into(),
origin: None,
},
version: Setting::Set {
value: 5,
origin: None,
},
})
);
let third = PartialSource::Server(PartialServer {
address: Setting::Set {
value: "bla".into(),
origin: None,
},
version: Setting::Unset,
});
left.merge(
third,
&mut MergeContext {
policy: MergePolicy::Override,
path: ConfigPath::root(),
provenance: &ProvenanceTracker::new(),
},
)
.unwrap();
assert_eq!(
left,
PartialSource::Server(PartialServer {
address: Setting::Set {
value: "bla".into(),
origin: None,
},
version: Setting::Set {
value: 5,
origin: None,
},
})
);
}
#[test]
fn merge_of_enum_overrides_when_allowed() {
let mut left = PartialSource::Server(PartialServer {
address: Setting::Unset,
version: Setting::Set {
value: 5,
origin: None,
},
});
let right = PartialSource::Pool(PartialPool {
address: Setting::Set {
value: "hello".into(),
origin: None,
},
count: Setting::Unset,
});
left.merge(
right,
&mut MergeContext {
policy: MergePolicy::Override,
path: ConfigPath::root(),
provenance: &ProvenanceTracker::new(),
},
)
.unwrap();
assert_eq!(
left,
PartialSource::Pool(PartialPool {
address: Setting::Set {
value: "hello".into(),
origin: None,
},
count: Setting::Unset,
})
);
}
#[test]
fn merge_of_enum_rejects_different_variants_when_not_overriding() {
let mut left = PartialSource::Server(PartialServer {
address: Setting::Unset,
version: Setting::Set {
value: 5,
origin: None,
},
});
let right = PartialSource::Pool(PartialPool {
address: Setting::Set {
value: "hello".into(),
origin: None,
},
count: Setting::Unset,
});
assert!(matches!(
left.merge(
right,
&mut MergeContext {
policy: MergePolicy::RejectOverlap,
path: ConfigPath::root(),
provenance: &ProvenanceTracker::new()
}
),
Err(ConfigError::MismatchedVariants {
current: "server",
incoming: "Pool",
..
})
));
}
}