Skip to main content

statime_config/tree/
merge.rs

1use std::{collections::HashMap, path::PathBuf};
2
3use crate::{ConfigError, tree::path::ConfigPath};
4
5/// Whether to merge by overriding values, or by rejecting any overlapping values.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum MergePolicy {
8    /// Reject any overlapping values and return an error if any is found.
9    RejectOverlap,
10    /// Override any overlapping values with the incoming value.
11    Override,
12}
13
14/// Where a configuration file/value is coming from.
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub enum Origin {
17    /// The source is the main configuration file.
18    MainConfig(PathBuf),
19    /// The source is a system configuration file.
20    SystemConfig(PathBuf),
21    /// The source is a built-in default value.
22    BuiltInDefault,
23}
24
25/// A unique identifier that represents an origin.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct OriginId(usize);
28
29impl OriginId {
30    /// The reserved id for [`Origin::BuiltInDefault`].
31    pub const BUILT_IN_DEFAULT: Self = Self(0);
32}
33
34/// Tracks the origin ids to specific origins.
35#[derive(Debug)]
36pub struct ProvenanceTracker {
37    origins: HashMap<OriginId, Origin>,
38}
39
40impl ProvenanceTracker {
41    /// Create an empty provenance tracker.
42    pub fn new() -> Self {
43        Self {
44            origins: HashMap::new(),
45        }
46    }
47
48    /// Track an origin, returns the origin id for that origin.
49    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        // ids start at one, since zero is reserved for the built-in default
57        let id = OriginId(self.origins.len() + 1);
58        self.origins.insert(id, origin);
59        id
60    }
61
62    /// Get the origin for a given origin id, if it is known.
63    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
77/// The context within the current merge operation. Generated code only ever
78/// walks it with [`MergeContext::at`]; the rest is the loader's business.
79pub 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    /// Run `f` with `field` appended to the path being merged.
96    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    /// The document an id stands for, for use in a diagnostic.
104    pub(crate) fn origin(&self, id: Option<OriginId>) -> Option<Origin> {
105        id.and_then(|id| self.provenance.get_origin(id)).cloned()
106    }
107}
108
109/// Allows the merging of two values following the merge policy in the merge
110/// context.
111pub 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        // it resolves without ever having been registered
129        assert_eq!(
130            tracker.get_origin(OriginId::BUILT_IN_DEFAULT),
131            Some(&Origin::BuiltInDefault)
132        );
133        // and registering it hands back the reserved id rather than a new one
134        assert_eq!(
135            tracker.track(Origin::BuiltInDefault),
136            OriginId::BUILT_IN_DEFAULT
137        );
138
139        // no tracked origin may collide with the reserved id
140        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}