ephemeris_files/
path_policy.rs1use facet::Facet;
4
5use crate::FilesError;
6use crate::path::{PathSelector, RepositoryPath};
7
8#[derive(Debug, Clone, Facet)]
14#[facet(deny_unknown_fields)]
15pub(crate) struct PathRuleState {
16 pub(crate) selector: PathSelector,
18 pub(crate) source: Option<PolicyValue>,
20 pub(crate) ignore: Option<PolicyValue>,
22 pub(crate) managed: Option<PolicyValue>,
24 pub(crate) format: Option<PolicyValue>,
26}
27
28#[derive(Debug, Clone, Facet)]
30#[facet(deny_unknown_fields)]
31pub(crate) struct PolicyValue {
32 pub(crate) value: bool,
34 pub(crate) priority: u32,
36}
37
38pub(crate) struct PathPolicy<'a> {
39 rules: &'a [PathRuleState],
40}
41
42impl<'a> PathPolicy<'a> {
43 pub(crate) fn new(rules: &'a [PathRuleState]) -> Result<Self, FilesError> {
48 if let Some(rule) = rules.iter().find(|rule| !rule.selector.valid()) {
49 return Err(FilesError::InvalidSelector {
50 selector: rule.selector.clone(),
51 });
52 }
53 Ok(Self { rules })
54 }
55
56 pub(crate) fn managed(&self, path: &RepositoryPath) -> Result<bool, FilesError> {
65 let mut best: Option<(u32, bool, bool)> = None;
66 for value in self
67 .rules
68 .iter()
69 .filter(|rule| rule.selector.matches(path))
70 .filter_map(|rule| rule.managed.as_ref())
71 {
72 match &mut best {
73 Some((priority, first, conflict)) if value.priority == *priority => {
74 *conflict |= value.value != *first;
75 }
76 Some((priority, _, _)) if value.priority > *priority => {}
77 _ => best = Some((value.priority, value.value, false)),
78 }
79 }
80 match best {
81 None => Ok(false),
82 Some((priority, _, true)) => Err(FilesError::ManagedConflict {
83 path: path.clone(),
84 priority,
85 }),
86 Some((_, value, false)) => Ok(value),
87 }
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use facet::Facet;
94
95 use super::{PathPolicy, PathRuleState, PolicyValue};
96 use crate::FilesError;
97 use crate::path::{PathSegment, PathSelector, RepositoryPath};
98
99 #[derive(Facet)]
100 struct Fixture {
101 rules: Vec<PathRuleState>,
102 cases: Vec<Case>,
103 }
104
105 #[derive(Facet)]
106 struct Case {
107 path: String,
108 managed: bool,
109 }
110
111 fn repo(path: &str) -> RepositoryPath {
112 RepositoryPath::try_from(path).expect("valid repository path")
113 }
114
115 fn exact(value: &str) -> PathSelector {
116 PathSelector::Exact {
117 segments: vec![PathSegment::Literal {
118 value: value.into(),
119 }],
120 }
121 }
122
123 fn managed_rule(selector: PathSelector, value: bool, priority: u32) -> PathRuleState {
124 PathRuleState {
125 selector,
126 source: None,
127 ignore: None,
128 format: None,
129 managed: Some(PolicyValue { value, priority }),
130 }
131 }
132
133 #[test]
134 fn shared_nix_fixture_resolves_the_same_managed_state() {
135 let fixture: Fixture =
138 facet_json::from_str(include_str!("../tests/data/path-policy-cases.json"))
139 .expect("shared path-policy fixture decodes");
140 let policy = PathPolicy::new(&fixture.rules).expect("fixture selectors are valid");
141
142 for case in fixture.cases {
143 assert_eq!(
144 policy.managed(&repo(&case.path)).unwrap(),
145 case.managed,
146 "{}",
147 case.path
148 );
149 }
150 }
151
152 #[test]
153 fn a_path_without_a_rule_is_not_managed() {
154 let rules = [managed_rule(exact("other"), true, 1000)];
155 assert!(
156 !PathPolicy::new(&rules)
157 .unwrap()
158 .managed(&repo("file"))
159 .unwrap()
160 );
161 }
162
163 #[test]
164 fn the_lower_priority_wins() {
165 let rules = [
166 managed_rule(exact("file"), true, 10),
167 managed_rule(exact("file"), false, 20),
168 ];
169 assert!(
170 PathPolicy::new(&rules)
171 .unwrap()
172 .managed(&repo("file"))
173 .unwrap()
174 );
175 }
176
177 #[test]
178 fn conflicting_values_at_equal_priority_are_an_error() {
179 let rules = [
180 managed_rule(exact("file"), true, 5),
181 managed_rule(exact("file"), false, 5),
182 ];
183 assert!(matches!(
184 PathPolicy::new(&rules).unwrap().managed(&repo("file")),
185 Err(FilesError::ManagedConflict {
186 priority: 5,
187 path: _
188 })
189 ));
190 }
191}