scratchstack_aspen/resource/
mod.rs1mod arn;
2
3use {
4 crate::{serutil::StringLikeList, AspenError, Context, PolicyVersion},
5 scratchstack_arn::Arn,
6 std::{
7 fmt::{Display, Formatter, Result as FmtResult},
8 str::FromStr,
9 },
10};
11
12pub use arn::ResourceArn;
13
14pub type ResourceList = StringLikeList<Resource>;
16
17#[derive(Clone, Debug, Eq, PartialEq)]
21pub enum Resource {
22 Any,
24
25 Arn(ResourceArn),
27}
28
29impl Resource {
30 #[inline]
32 pub fn is_any(&self) -> bool {
33 matches!(self, Self::Any)
34 }
35
36 pub fn matches(&self, context: &Context, pv: PolicyVersion, candidate: &Arn) -> Result<bool, AspenError> {
61 match self {
62 Self::Any => Ok(true),
63 Self::Arn(pattern) => pattern.matches(context, pv, candidate),
64 }
65 }
66}
67
68impl FromStr for Resource {
69 type Err = AspenError;
70
71 fn from_str(s: &str) -> Result<Self, Self::Err> {
72 if s == "*" {
73 return Ok(Self::Any);
74 }
75
76 let pattern = ResourceArn::from_str(s)?;
77 Ok(Self::Arn(pattern))
78 }
79}
80
81impl Display for Resource {
82 fn fmt(&self, f: &mut Formatter) -> FmtResult {
83 match self {
84 Self::Any => f.write_str("*"),
85 Self::Arn(arn_pattern) => f.write_str(&arn_pattern.to_string()),
86 }
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use {
93 crate::{serutil::JsonRep, Resource, ResourceArn, ResourceList},
94 indoc::indoc,
95 pretty_assertions::assert_eq,
96 std::{panic::catch_unwind, str::FromStr},
97 };
98
99 #[test_log::test]
100 fn deserialize_resource_list_star() {
101 let resource_list: ResourceList = serde_json::from_str("\"*\"").unwrap();
102 assert_eq!(resource_list.kind(), JsonRep::Single);
103 let v = resource_list.to_vec();
104 assert_eq!(v, vec![&Resource::Any]);
105 assert!(!v.is_empty());
106 }
107
108 #[test_log::test]
109 fn check_from() {
110 let ap = ResourceArn::from_str("arn:*:ec*:us-*-2:123?56789012:instance/*").unwrap();
111 let rl1: ResourceList = Resource::Arn(ap.clone()).into();
112 let rl2: ResourceList = Resource::Arn(ap.clone()).into();
113 let rl3: ResourceList = vec![Resource::Arn(ap.clone())].into();
114 let rl4: ResourceList = vec![Resource::Arn(ap.clone())].into();
115
116 assert_eq!(rl1, rl2);
117 assert_eq!(rl1, rl3);
118 assert_eq!(rl1, rl4);
119 assert_eq!(rl2, rl3);
120 assert_eq!(rl2, rl4);
121 assert_eq!(rl3, rl4);
122 assert_eq!(rl2, rl1);
123 assert_eq!(rl3, rl1);
124 assert_eq!(rl4, rl1);
125 assert_eq!(rl3, rl2);
126 assert_eq!(rl4, rl2);
127 assert_eq!(rl4, rl3);
128
129 assert!(!rl1.is_empty());
130 assert!(!rl2.is_empty());
131 assert!(!rl3.is_empty());
132 assert!(!rl4.is_empty());
133 assert_eq!(rl1.len(), 1);
134 assert_eq!(rl2.len(), 1);
135 assert_eq!(rl3.len(), 1);
136 assert_eq!(rl4.len(), 1);
137
138 assert_eq!(rl1[0], Resource::Arn(ap.clone()));
139 assert_eq!(rl2[0], Resource::Arn(ap));
140
141 let e = catch_unwind(|| {
142 println!("This will not print: {}", rl1[1]);
143 })
144 .unwrap_err();
145 assert_eq!(*e.downcast::<String>().unwrap(), "index out of bounds: the len is 1 but the index is 1");
146
147 assert_eq!(format!("{rl1}"), r#""arn:*:ec*:us-*-2:123?56789012:instance/*""#);
148 assert_eq!(
149 format!("{rl3}"),
150 indoc! { r#"
151 [
152 "arn:*:ec*:us-*-2:123?56789012:instance/*"
153 ]"# }
154 );
155 }
156
157 #[test_log::test]
158 fn check_bad() {
159 let e = Resource::from_str("arn:aws").unwrap_err();
160 assert_eq!(e.to_string(), "Invalid resource: arn:aws");
161 }
162
163 #[test_log::test]
164 fn check_derived() {
165 let r1a = Resource::from_str("arn:aws:ec2:us-east-2:123456789012:instance/*").unwrap();
166 let r1b = Resource::from_str("arn:aws:ec2:us-east-2:123456789012:instance/*").unwrap();
167 let r2 = Resource::Any;
168
169 assert_eq!(r1a, r1b);
170 assert_ne!(r1a, r2);
171 assert_eq!(r1a, r1a.clone());
172
173 assert_eq!(r1a.to_string(), "arn:aws:ec2:us-east-2:123456789012:instance/*");
174 assert_eq!(r2.to_string(), "*");
175 }
176}