1use {
2 crate::{eval::regex_from_glob, serutil::StringLikeList, AspenError},
3 log::debug,
4 std::{
5 fmt::{Display, Formatter, Result as FmtResult},
6 str::FromStr,
7 },
8};
9
10pub type ActionList = StringLikeList<Action>;
12
13#[derive(Clone, Debug)]
18pub enum Action {
19 Any,
21
22 Specific(SpecificActionDetails),
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct SpecificActionDetails {
28 service: String,
30
31 api: String,
33}
34
35impl PartialEq for Action {
36 fn eq(&self, other: &Self) -> bool {
37 match (self, other) {
38 (Self::Any, Self::Any) => true,
39 (Self::Specific(my_details), Self::Specific(other_details)) => my_details == other_details,
40 _ => false,
41 }
42 }
43}
44
45impl Eq for Action {}
46
47impl Action {
48 pub fn new<S: Into<String>, A: Into<String>>(service: S, api: A) -> Result<Self, AspenError> {
60 let service = service.into();
61 let api = api.into();
62
63 if service.is_empty() {
64 debug!("Action '{service}:{api}' has an empty service.");
65 return Err(AspenError::InvalidAction(format!("{service}:{api}")));
66 }
67
68 if api.is_empty() {
69 debug!("Action '{service}:{api}' has an empty API.");
70 return Err(AspenError::InvalidAction(format!("{service}:{api}")));
71 }
72
73 if !service.is_ascii() || !api.is_ascii() {
74 debug!("Action '{service}:{api}' is not ASCII.");
75 return Err(AspenError::InvalidAction(format!("{service}:{api}")));
76 }
77
78 for (i, c) in service.bytes().enumerate() {
79 if !c.is_ascii_alphanumeric() && !(i > 0 && i < service.len() - 1 && (c == b'-' || c == b'_')) {
80 debug!("Action '{service}:{api}' has an invalid service.");
81 return Err(AspenError::InvalidAction(format!("{service}:{api}")));
82 }
83 }
84
85 for (i, c) in api.bytes().enumerate() {
86 if !c.is_ascii_alphanumeric()
87 && c != b'*'
88 && c != b'?'
89 && !(i > 0 && i < api.len() - 1 && (c == b'-' || c == b'_'))
90 {
91 debug!("Action '{service}:{api}' has an invalid API.");
92 return Err(AspenError::InvalidAction(format!("{service}:{api}")));
93 }
94 }
95
96 Ok(Action::Specific(SpecificActionDetails {
97 service,
98 api,
99 }))
100 }
101
102 #[inline]
104 pub fn is_any(&self) -> bool {
105 matches!(self, Self::Any)
106 }
107
108 #[inline]
110 pub fn specific(&self) -> Option<(&str, &str)> {
111 match self {
112 Self::Any => None,
113 Self::Specific(details) => Some((&details.service, &details.api)),
114 }
115 }
116
117 #[inline]
119 pub fn service(&self) -> &str {
120 match self {
121 Self::Any => "*",
122 Self::Specific(SpecificActionDetails {
123 service,
124 ..
125 }) => service,
126 }
127 }
128
129 #[inline]
131 pub fn api(&self) -> &str {
132 match self {
133 Self::Any => "*",
134 Self::Specific(SpecificActionDetails {
135 api,
136 ..
137 }) => api,
138 }
139 }
140
141 pub fn matches(&self, service: &str, api: &str) -> bool {
143 match self {
144 Self::Any => true,
145 Self::Specific(SpecificActionDetails {
146 service: self_service,
147 api: self_api,
148 }) => {
149 if self_service == service {
150 regex_from_glob(self_api, false).is_match(api)
151 } else {
152 false
153 }
154 }
155 }
156 }
157}
158
159impl FromStr for Action {
160 type Err = AspenError;
161 fn from_str(v: &str) -> Result<Self, Self::Err> {
162 if v == "*" {
163 return Ok(Self::Any);
164 }
165
166 let parts: Vec<&str> = v.split(':').collect();
167 if parts.len() != 2 {
168 return Err(AspenError::InvalidAction(v.to_string()));
169 }
170
171 let service = parts[0];
172 let api = parts[1];
173
174 Action::new(service, api)
175 }
176}
177
178impl Display for Action {
179 fn fmt(&self, f: &mut Formatter) -> FmtResult {
180 match self {
181 Self::Any => f.write_str("*"),
182 Self::Specific(details) => Display::fmt(details, f),
183 }
184 }
185}
186
187impl Display for SpecificActionDetails {
188 fn fmt(&self, f: &mut Formatter) -> FmtResult {
189 write!(f, "{}:{}", self.service, self.api)
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use {
196 crate::{Action, ActionList},
197 indoc::indoc,
198 pretty_assertions::{assert_eq, assert_ne},
199 std::{panic::catch_unwind, str::FromStr},
200 };
201
202 #[test_log::test]
203 fn test_eq() {
204 let a1a: ActionList = Action::new("s1", "a1").unwrap().into();
205 let a1b: ActionList = vec![Action::new("s1", "a1").unwrap()].into();
206 let a2a: ActionList = Action::new("s2", "a1").unwrap().into();
207 let a2b: ActionList = vec![Action::new("s2", "a1").unwrap()].into();
208 let a3a: ActionList = Action::new("s1", "a2").unwrap().into();
209 let a3b: ActionList = vec![Action::new("s1", "a2").unwrap()].into();
210 let a4a: ActionList = vec![].into();
211 let a4b: ActionList = vec![].into();
212
213 assert_eq!(a1a, a1a.clone());
214 assert_eq!(a1b, a1b.clone());
215 assert_eq!(a2a, a2a.clone());
216 assert_eq!(a2b, a2b.clone());
217 assert_eq!(a3a, a3a.clone());
218 assert_eq!(a3b, a3b.clone());
219 assert_eq!(a4a, a4a.clone());
220 assert_eq!(a4b, a4b.clone());
221
222 assert_eq!(a1a.len(), 1);
223 assert_eq!(a1b.len(), 1);
224 assert_eq!(a2a.len(), 1);
225 assert_eq!(a2b.len(), 1);
226 assert_eq!(a3a.len(), 1);
227 assert_eq!(a3b.len(), 1);
228 assert_eq!(a4a.len(), 0);
229 assert_eq!(a4b.len(), 0);
230
231 assert!(!a1a.is_empty());
232 assert!(!a1b.is_empty());
233 assert!(!a2a.is_empty());
234 assert!(!a2b.is_empty());
235 assert!(!a3a.is_empty());
236 assert!(!a3b.is_empty());
237 assert!(a4a.is_empty());
238 assert!(a4b.is_empty());
239
240 assert_eq!(a1a, a1b);
241 assert_eq!(a1b, a1a);
242 assert_eq!(a2a, a2b);
243 assert_eq!(a2b, a2a);
244 assert_eq!(a3a, a3b);
245 assert_eq!(a3b, a3a);
246 assert_eq!(a4a, a4b);
247 assert_eq!(a4b, a4a);
248
249 assert_ne!(a1a, a2a);
250 assert_ne!(a1a, a2b);
251 assert_ne!(a1a, a3a);
252 assert_ne!(a1a, a3b);
253 assert_ne!(a1a, a4a);
254 assert_ne!(a1a, a4b);
255 assert_ne!(a2a, a1a);
256 assert_ne!(a2b, a1a);
257 assert_ne!(a3a, a1a);
258 assert_ne!(a3b, a1a);
259 assert_ne!(a4a, a1a);
260 assert_ne!(a4b, a1a);
261
262 assert_ne!(a1b, a2a);
263 assert_ne!(a1b, a2b);
264 assert_ne!(a1b, a3a);
265 assert_ne!(a1b, a3b);
266 assert_ne!(a1b, a4a);
267 assert_ne!(a1b, a4b);
268 assert_ne!(a2a, a1b);
269 assert_ne!(a2b, a1b);
270 assert_ne!(a3a, a1b);
271 assert_ne!(a3b, a1b);
272 assert_ne!(a4a, a1b);
273 assert_ne!(a4b, a1b);
274
275 assert_ne!(a2a, a3a);
276 assert_ne!(a2a, a3b);
277 assert_ne!(a2a, a4a);
278 assert_ne!(a2a, a4b);
279 assert_ne!(a3a, a2a);
280 assert_ne!(a3b, a2a);
281 assert_ne!(a4a, a2a);
282 assert_ne!(a4b, a2a);
283
284 assert_ne!(a2b, a3a);
285 assert_ne!(a2b, a3b);
286 assert_ne!(a2b, a4a);
287 assert_ne!(a2b, a4b);
288 assert_ne!(a3a, a2b);
289 assert_ne!(a3b, a2b);
290 assert_ne!(a4a, a2b);
291 assert_ne!(a4b, a2b);
292
293 assert_ne!(a3a, a4a);
294 assert_ne!(a3a, a4b);
295 assert_ne!(a4a, a3a);
296 assert_ne!(a4b, a3a);
297
298 assert_ne!(a3b, a4a);
299 assert_ne!(a3b, a4b);
300 assert_ne!(a4a, a3b);
301 assert_ne!(a4b, a3b);
302
303 assert_eq!(Action::Any, Action::Any);
304 }
305
306 #[test_log::test]
307 fn test_from() {
308 let a1a: ActionList = vec![Action::new("s1", "a1").unwrap()].into();
309 let a1b: ActionList = Action::new("s1", "a1").unwrap().into();
310 let a2a: ActionList = vec![Action::Any].into();
311
312 assert_eq!(a1a, a1b);
313 assert_eq!(a1b, a1a);
314 assert_ne!(a1a, a2a);
315
316 assert_eq!(a1a[0], a1b[0]);
317
318 assert_eq!(
319 format!("{a1a}"),
320 indoc! {r#"
321 [
322 "s1:a1"
323 ]"#}
324 );
325 assert_eq!(format!("{a1b}"), r#""s1:a1""#);
326 assert_eq!(
327 format!("{a2a}"),
328 indoc! {r#"
329 [
330 "*"
331 ]"#}
332 );
333
334 assert_eq!(format!("{}", a2a[0]), "*");
335
336 let e = catch_unwind(|| {
337 println!("This will not be printed: {}", a1b[1]);
338 })
339 .unwrap_err();
340 assert_eq!(*e.downcast::<String>().unwrap(), "index out of bounds: the len is 1 but the index is 1");
341 }
342
343 #[test_log::test]
344 fn test_bad_strings() {
345 assert_eq!(Action::from_str("").unwrap_err().to_string(), "Invalid action: ");
346 assert_eq!(Action::from_str("ec2:").unwrap_err().to_string(), "Invalid action: ec2:");
347 assert_eq!(
348 Action::from_str(":DescribeInstances").unwrap_err().to_string(),
349 "Invalid action: :DescribeInstances"
350 );
351 assert_eq!(
352 Action::from_str("🦀:DescribeInstances").unwrap_err().to_string(),
353 "Invalid action: 🦀:DescribeInstances"
354 );
355 assert_eq!(Action::from_str("ec2:🦀").unwrap_err().to_string(), "Invalid action: ec2:🦀");
356 assert_eq!(
357 Action::from_str("-ec2:DescribeInstances").unwrap_err().to_string(),
358 "Invalid action: -ec2:DescribeInstances"
359 );
360 assert_eq!(
361 Action::from_str("_ec2:DescribeInstances").unwrap_err().to_string(),
362 "Invalid action: _ec2:DescribeInstances"
363 );
364 assert_eq!(
365 Action::from_str("ec2-:DescribeInstances").unwrap_err().to_string(),
366 "Invalid action: ec2-:DescribeInstances"
367 );
368 assert_eq!(
369 Action::from_str("ec2_:DescribeInstances").unwrap_err().to_string(),
370 "Invalid action: ec2_:DescribeInstances"
371 );
372 assert_eq!(
373 Action::from_str("ec2:-DescribeInstances").unwrap_err().to_string(),
374 "Invalid action: ec2:-DescribeInstances"
375 );
376 assert_eq!(
377 Action::from_str("ec2:_DescribeInstances").unwrap_err().to_string(),
378 "Invalid action: ec2:_DescribeInstances"
379 );
380 assert_eq!(
381 Action::from_str("ec2:DescribeInstances-").unwrap_err().to_string(),
382 "Invalid action: ec2:DescribeInstances-"
383 );
384 assert_eq!(
385 Action::from_str("ec2:DescribeInstances_").unwrap_err().to_string(),
386 "Invalid action: ec2:DescribeInstances_"
387 );
388
389 assert_eq!(Action::from_str("e_c-2:De-scribe_Instances").unwrap().service(), "e_c-2");
390 assert_eq!(Action::from_str("e_c-2:De-scribe_Instances").unwrap().api(), "De-scribe_Instances");
391 assert!(Action::from_str("e_c-2:De-scribe_Instances").unwrap().specific().is_some());
392 assert!(!Action::from_str("e_c-2:De-scribe_Instances").unwrap().is_any());
393 assert_eq!(Action::from_str("*").unwrap().service(), "*");
394 assert_eq!(Action::from_str("*").unwrap().api(), "*");
395 assert!(Action::from_str("*").unwrap().is_any());
396 assert!(Action::from_str("*").unwrap().specific().is_none());
397 }
398}