1use std::{collections::HashMap, path::PathBuf};
2
3use crate::{ConfigError, tree::path::ConfigPath};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum MergePolicy {
8 RejectOverlap,
10 Override,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub enum Origin {
17 MainConfig(PathBuf),
19 SystemConfig(PathBuf),
21 BuiltInDefault,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct OriginId(usize);
28
29impl OriginId {
30 pub const BUILT_IN_DEFAULT: Self = Self(0);
32}
33
34#[derive(Debug)]
36pub struct ProvenanceTracker {
37 origins: HashMap<OriginId, Origin>,
38}
39
40impl ProvenanceTracker {
41 pub fn new() -> Self {
43 Self {
44 origins: HashMap::new(),
45 }
46 }
47
48 pub fn track(&mut self, origin: Origin) -> OriginId {
50 if origin == Origin::BuiltInDefault {
51 return OriginId::BUILT_IN_DEFAULT;
52 }
53 if let Some((id, _)) = self.origins.iter().find(|(_, o)| *o == &origin) {
54 return *id;
55 }
56 let id = OriginId(self.origins.len() + 1);
58 self.origins.insert(id, origin);
59 id
60 }
61
62 pub fn get_origin(&self, id: OriginId) -> Option<&Origin> {
64 if id == OriginId::BUILT_IN_DEFAULT {
65 return Some(&Origin::BuiltInDefault);
66 }
67 self.origins.get(&id)
68 }
69}
70
71impl Default for ProvenanceTracker {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77pub struct MergeContext<'a> {
80 #[doc(hidden)]
81 pub policy: MergePolicy,
82 pub path: ConfigPath,
83 provenance: &'a ProvenanceTracker,
84}
85
86impl<'a> MergeContext<'a> {
87 pub(crate) fn new(policy: MergePolicy, provenance: &'a ProvenanceTracker) -> Self {
88 Self {
89 policy,
90 path: ConfigPath::root(),
91 provenance,
92 }
93 }
94
95 pub fn at<R>(&mut self, field: &'static str, f: impl FnOnce(&mut Self) -> R) -> R {
97 self.path.push_field(field);
98 let result = f(self);
99 self.path.pop();
100 result
101 }
102
103 pub(crate) fn origin(&self, id: Option<OriginId>) -> Option<Origin> {
105 id.and_then(|id| self.provenance.get_origin(id)).cloned()
106 }
107}
108
109pub trait Merge {
112 fn merge(&mut self, incoming: Self, context: &mut MergeContext<'_>) -> Result<(), ConfigError>;
113}
114
115#[cfg(test)]
116mod tests {
117 use crate::{
118 Setting,
119 fixture::{PartialPool, PartialServer, PartialSource},
120 };
121
122 use super::*;
123
124 #[test]
125 fn the_built_in_default_origin_is_reserved() {
126 let mut tracker = ProvenanceTracker::new();
127
128 assert_eq!(
130 tracker.get_origin(OriginId::BUILT_IN_DEFAULT),
131 Some(&Origin::BuiltInDefault)
132 );
133 assert_eq!(
135 tracker.track(Origin::BuiltInDefault),
136 OriginId::BUILT_IN_DEFAULT
137 );
138
139 let first = tracker.track(Origin::MainConfig("/etc/ntp.toml".into()));
141 let second = tracker.track(Origin::SystemConfig("/etc/ntp.d/a.toml".into()));
142 assert_ne!(first, OriginId::BUILT_IN_DEFAULT);
143 assert_ne!(second, OriginId::BUILT_IN_DEFAULT);
144 assert_ne!(first, second);
145 assert_eq!(
146 tracker.get_origin(first),
147 Some(&Origin::MainConfig("/etc/ntp.toml".into()))
148 );
149 }
150
151 #[test]
152 fn merge_of_enum_merges_identical() {
153 let mut left = PartialSource::Server(PartialServer {
154 address: Setting::Unset,
155 version: Setting::Set {
156 value: 5,
157 origin: None,
158 },
159 });
160
161 let right = PartialSource::Server(PartialServer {
162 address: Setting::Set {
163 value: "hello".into(),
164 origin: None,
165 },
166 version: Setting::Unset,
167 });
168
169 left.merge(
170 right,
171 &mut MergeContext {
172 policy: MergePolicy::RejectOverlap,
173 path: ConfigPath::root(),
174 provenance: &ProvenanceTracker::new(),
175 },
176 )
177 .unwrap();
178 assert_eq!(
179 left,
180 PartialSource::Server(PartialServer {
181 address: Setting::Set {
182 value: "hello".into(),
183 origin: None,
184 },
185 version: Setting::Set {
186 value: 5,
187 origin: None,
188 },
189 })
190 );
191
192 let third = PartialSource::Server(PartialServer {
193 address: Setting::Set {
194 value: "bla".into(),
195 origin: None,
196 },
197 version: Setting::Unset,
198 });
199
200 left.merge(
201 third,
202 &mut MergeContext {
203 policy: MergePolicy::Override,
204 path: ConfigPath::root(),
205 provenance: &ProvenanceTracker::new(),
206 },
207 )
208 .unwrap();
209 assert_eq!(
210 left,
211 PartialSource::Server(PartialServer {
212 address: Setting::Set {
213 value: "bla".into(),
214 origin: None,
215 },
216 version: Setting::Set {
217 value: 5,
218 origin: None,
219 },
220 })
221 );
222 }
223
224 #[test]
225 fn merge_of_enum_overrides_when_allowed() {
226 let mut left = PartialSource::Server(PartialServer {
227 address: Setting::Unset,
228 version: Setting::Set {
229 value: 5,
230 origin: None,
231 },
232 });
233
234 let right = PartialSource::Pool(PartialPool {
235 address: Setting::Set {
236 value: "hello".into(),
237 origin: None,
238 },
239 count: Setting::Unset,
240 });
241
242 left.merge(
243 right,
244 &mut MergeContext {
245 policy: MergePolicy::Override,
246 path: ConfigPath::root(),
247 provenance: &ProvenanceTracker::new(),
248 },
249 )
250 .unwrap();
251
252 assert_eq!(
253 left,
254 PartialSource::Pool(PartialPool {
255 address: Setting::Set {
256 value: "hello".into(),
257 origin: None,
258 },
259 count: Setting::Unset,
260 })
261 );
262 }
263
264 #[test]
265 fn merge_of_enum_rejects_different_variants_when_not_overriding() {
266 let mut left = PartialSource::Server(PartialServer {
267 address: Setting::Unset,
268 version: Setting::Set {
269 value: 5,
270 origin: None,
271 },
272 });
273
274 let right = PartialSource::Pool(PartialPool {
275 address: Setting::Set {
276 value: "hello".into(),
277 origin: None,
278 },
279 count: Setting::Unset,
280 });
281
282 assert!(matches!(
283 left.merge(
284 right,
285 &mut MergeContext {
286 policy: MergePolicy::RejectOverlap,
287 path: ConfigPath::root(),
288 provenance: &ProvenanceTracker::new()
289 }
290 ),
291 Err(ConfigError::MismatchedVariants {
292 current: "server",
293 incoming: "Pool",
294 ..
295 })
296 ));
297 }
298}