1use std::cmp::Ordering;
4use std::fmt::Display;
5
6#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
9pub enum AccessLevel {
10 Pull,
12
13 Read,
15
16 Write,
18
19 Manage,
21}
22
23#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct Access<C = ()> {
34 pub conditions: Option<C>,
35 pub level: AccessLevel,
36}
37
38impl<C> Display for Access<C> {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 let s = match self.level {
41 AccessLevel::Pull => "pull",
42 AccessLevel::Read => "read",
43 AccessLevel::Write => "write",
44 AccessLevel::Manage => "manage",
45 };
46
47 write!(f, "{}", s)
48 }
49}
50
51impl<C> Access<C> {
52 pub fn pull() -> Self {
54 Self {
55 level: AccessLevel::Pull,
56 conditions: None,
57 }
58 }
59
60 pub fn read() -> Self {
62 Self {
63 level: AccessLevel::Read,
64 conditions: None,
65 }
66 }
67
68 pub fn write() -> Self {
70 Self {
71 level: AccessLevel::Write,
72 conditions: None,
73 }
74 }
75
76 pub fn manage() -> Self {
78 Self {
79 level: AccessLevel::Manage,
80 conditions: None,
81 }
82 }
83
84 pub fn with_conditions(mut self, conditions: C) -> Self {
86 self.conditions = Some(conditions);
87 self
88 }
89
90 pub fn is_pull(&self) -> bool {
92 matches!(self.level, AccessLevel::Pull)
93 }
94
95 pub fn is_read(&self) -> bool {
97 matches!(self.level, AccessLevel::Read)
98 }
99
100 pub fn is_write(&self) -> bool {
102 matches!(self.level, AccessLevel::Write)
103 }
104
105 pub fn is_manage(&self) -> bool {
107 matches!(self.level, AccessLevel::Manage)
108 }
109}
110
111impl<C: PartialOrd> PartialOrd for Access<C> {
112 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
113 match (self.conditions.as_ref(), other.conditions.as_ref()) {
114 (Some(self_cond), Some(other_cond)) => {
116 match self_cond.partial_cmp(other_cond) {
117 Some(Ordering::Greater | Ordering::Equal) => {
120 match self.level.cmp(&other.level) {
121 Ordering::Less => Some(Ordering::Less),
122 Ordering::Equal | Ordering::Greater => Some(Ordering::Greater),
123 }
124 }
125 Some(Ordering::Less) => Some(Ordering::Less),
126 None => None,
127 }
128 }
129 (None, Some(_)) => match self.level.cmp(&other.level) {
130 Ordering::Less => Some(Ordering::Less),
131 Ordering::Equal | Ordering::Greater => Some(Ordering::Greater),
132 },
133 _ => Some(self.level.cmp(&other.level)),
134 }
135 }
136}
137
138impl<C: PartialOrd + Eq> Ord for Access<C> {
139 fn cmp(&self, other: &Self) -> Ordering {
140 self.partial_cmp(other).unwrap_or(Ordering::Less)
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use std::cmp::Ordering;
147
148 use crate::Access;
149
150 #[derive(Debug, Clone, PartialEq, Eq)]
153 struct PathCondition(String);
154
155 impl PartialOrd for PathCondition {
156 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
157 let self_parts: Vec<_> = self.0.split('/').filter(|s| !s.is_empty()).collect();
158 let other_parts: Vec<_> = other.0.split('/').filter(|s| !s.is_empty()).collect();
159
160 let min_len = self_parts.len().min(other_parts.len());
161 let is_prefix = self_parts[..min_len] == other_parts[..min_len];
162
163 if is_prefix {
164 match self_parts.len().cmp(&other_parts.len()) {
165 Ordering::Less => Some(Ordering::Greater),
166 Ordering::Equal => Some(Ordering::Equal),
167 Ordering::Greater => Some(Ordering::Less),
168 }
169 } else {
170 None
171 }
172 }
173 }
174
175 #[test]
176 fn path_condition_comparators() {
177 let root_access = Access::read().with_conditions(PathCondition("/root".to_string()));
178 let private_access =
179 Access::read().with_conditions(PathCondition("/root/private".to_string()));
180 let public_access =
181 Access::read().with_conditions(PathCondition("/root/public".to_string()));
182
183 assert!(root_access >= private_access);
185 assert!(root_access >= public_access);
186
187 assert!(!(private_access >= public_access));
189 assert!(!(private_access <= public_access));
190
191 let read_access_to_root =
192 Access::read().with_conditions(PathCondition("/root".to_string()));
193 let requested_write_access_to_sub_path =
194 Access::write().with_conditions(PathCondition("/root/private".to_string()));
195
196 assert!(requested_write_access_to_sub_path < read_access_to_root);
197
198 let unconditional_read = Access::<PathCondition>::read();
199 assert!(unconditional_read > public_access);
200 }
201
202 #[derive(Debug, Clone, PartialOrd, PartialEq, Eq)]
204 struct ExpiryTimestamp(u64);
205
206 #[test]
207 fn expiry_timestamp_access_ordering() {
208 let access_expires_soon = Access::read().with_conditions(ExpiryTimestamp(10));
209 let access_expires_later = Access::read().with_conditions(ExpiryTimestamp(100));
210
211 assert!(access_expires_later > access_expires_soon);
213
214 assert!(access_expires_soon < access_expires_later);
216
217 const NOW: ExpiryTimestamp = ExpiryTimestamp(50);
221 let requested_read_access = Access::read().with_conditions(NOW);
222
223 assert!(access_expires_soon < requested_read_access);
226
227 assert!(access_expires_later >= requested_read_access);
230
231 let requested_pull_access = Access::pull().with_conditions(NOW);
235 assert!(access_expires_soon < requested_pull_access);
236
237 let requested_write_access = Access::write().with_conditions(NOW);
240 assert!(access_expires_later < requested_write_access);
241
242 let requested_read_access = Access::read().with_conditions(NOW);
244 let access_no_expiry = Access::<ExpiryTimestamp>::read();
245 assert!(access_no_expiry > requested_read_access);
246 }
247}