ephemeris_forge_github/
permission.rs1use std::fmt;
2use std::str::FromStr;
3
4use ephemeris_forge::Observed;
5use facet::Facet;
6use facet_value::Value;
7
8#[derive(Clone, Debug, Eq, Facet, PartialEq)]
13#[facet(proxy = Value)]
14#[repr(u8)]
15pub enum Permission {
16 Pull,
18 Triage,
20 Push,
22 Maintain,
24 Admin,
26 Custom(RoleName),
28}
29
30#[derive(Clone, Debug, Eq, Facet, PartialEq)]
32#[facet(proxy = String)]
33pub struct RoleName(String);
34
35#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
37pub enum PermissionError {
38 #[error("unknown permission {name:?}")]
40 Unknown {
41 name: String,
43 },
44 #[error("a permission is a name or an object with a single custom role name")]
46 Shape,
47 #[error("custom role name cannot be empty")]
49 EmptyRole,
50}
51
52impl Permission {
53 #[must_use]
58 pub fn from_role_name(raw: &str) -> Option<Self> {
59 match raw {
60 "" => None,
61 "read" => Some(Self::Pull),
62 "write" => Some(Self::Push),
63 other => Some(
64 other
65 .parse()
66 .unwrap_or_else(|_unknown| Self::Custom(RoleName(other.to_owned()))),
67 ),
68 }
69 }
70
71 #[must_use]
76 pub fn observe(role: &str, flagged: Option<Self>) -> Observed<Self> {
77 match Self::from_role_name(role) {
78 Some(custom @ Self::Custom(_)) => Observed::Known(custom),
79 named => flagged
80 .or(named)
81 .map_or_else(|| Observed::Unknown(role.to_owned()), Observed::Known),
82 }
83 }
84
85 #[must_use]
87 pub fn as_str(&self) -> &str {
88 match self {
89 Self::Pull => "pull",
90 Self::Triage => "triage",
91 Self::Push => "push",
92 Self::Maintain => "maintain",
93 Self::Admin => "admin",
94 Self::Custom(role) => role.as_str(),
95 }
96 }
97
98 #[must_use]
100 pub const fn invitation_name(&self) -> Option<&'static str> {
101 match self {
102 Self::Pull => Some("read"),
103 Self::Triage => Some("triage"),
104 Self::Push => Some("write"),
105 Self::Maintain => Some("maintain"),
106 Self::Admin => Some("admin"),
107 Self::Custom(_) => None,
108 }
109 }
110
111 #[must_use]
113 pub const fn custom(&self) -> Option<&RoleName> {
114 match self {
115 Self::Custom(role) => Some(role),
116 Self::Pull | Self::Triage | Self::Push | Self::Maintain | Self::Admin => None,
117 }
118 }
119}
120
121impl RoleName {
122 #[must_use]
124 pub fn as_str(&self) -> &str {
125 &self.0
126 }
127}
128
129impl FromStr for Permission {
130 type Err = PermissionError;
131
132 fn from_str(raw: &str) -> Result<Self, Self::Err> {
133 match raw {
134 "pull" => Ok(Self::Pull),
135 "triage" => Ok(Self::Triage),
136 "push" => Ok(Self::Push),
137 "maintain" => Ok(Self::Maintain),
138 "admin" => Ok(Self::Admin),
139 _ => Err(PermissionError::Unknown {
140 name: raw.to_owned(),
141 }),
142 }
143 }
144}
145
146impl TryFrom<Value> for Permission {
147 type Error = PermissionError;
148
149 fn try_from(value: Value) -> Result<Self, Self::Error> {
150 if let Some(name) = value.as_string() {
151 return name.as_str().parse();
152 }
153 let role = value
154 .as_object()
155 .filter(|object| object.len() == 1)
156 .and_then(|object| object.get("custom"))
157 .and_then(Value::as_string)
158 .ok_or(PermissionError::Shape)?;
159 Ok(Self::Custom(RoleName::try_from(role.as_str().to_owned())?))
160 }
161}
162
163impl TryFrom<&Permission> for Value {
164 type Error = PermissionError;
165
166 fn try_from(value: &Permission) -> Result<Self, Self::Error> {
167 Ok(match value {
168 Permission::Custom(role) => {
169 let mut object = facet_value::VObject::new();
170 object.insert("custom", Self::from(role.as_str()));
171 object.into()
172 }
173 named @ (Permission::Pull
174 | Permission::Triage
175 | Permission::Push
176 | Permission::Maintain
177 | Permission::Admin) => Self::from(named.as_str()),
178 })
179 }
180}
181
182impl TryFrom<String> for RoleName {
183 type Error = PermissionError;
184
185 fn try_from(value: String) -> Result<Self, Self::Error> {
186 if value.is_empty() {
187 Err(PermissionError::EmptyRole)
188 } else {
189 Ok(Self(value))
190 }
191 }
192}
193
194impl TryFrom<&RoleName> for String {
195 type Error = PermissionError;
196
197 fn try_from(value: &RoleName) -> Result<Self, Self::Error> {
198 Ok(value.0.clone())
199 }
200}
201
202impl fmt::Display for Permission {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 match self {
205 Self::Custom(role) => write!(f, "custom role {role}"),
206 named @ (Self::Pull | Self::Triage | Self::Push | Self::Maintain | Self::Admin) => {
207 f.write_str(named.as_str())
208 }
209 }
210 }
211}
212
213impl fmt::Display for RoleName {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 self.0.fmt(f)
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::{Permission, RoleName};
222
223 #[test]
224 fn the_five_names_decode_from_strings() {
225 let permission: Permission = facet_json::from_str(r#""maintain""#).expect("decodes");
226 assert_eq!(permission, Permission::Maintain);
227 }
228
229 #[test]
230 fn a_custom_role_decodes_from_its_object_and_encodes_back() {
231 let permission: Permission =
232 facet_json::from_str(r#"{"custom":"release-manager"}"#).expect("decodes");
233 let role = RoleName::try_from("release-manager".to_owned()).expect("valid");
234 assert_eq!(permission, Permission::Custom(role));
235 assert_eq!(
236 facet_json::to_string(&permission).expect("encodes"),
237 r#"{"custom":"release-manager"}"#
238 );
239 }
240
241 #[test]
242 fn other_shapes_are_rejected() {
243 for input in [
244 r#""write""#,
245 r#""release-manager""#,
246 r#"{"custom":""}"#,
247 r#"{"custom":"a","extra":"b"}"#,
248 r#"{"role":"a"}"#,
249 "{}",
250 "3",
251 ] {
252 facet_json::from_str::<Permission>(input).expect_err(input);
253 }
254 }
255
256 #[test]
257 fn live_role_names_map_to_permissions() {
258 assert_eq!(Permission::from_role_name("write"), Some(Permission::Push));
259 assert_eq!(
260 Permission::from_role_name("triage"),
261 Some(Permission::Triage)
262 );
263 assert_eq!(
264 Permission::from_role_name("deployer")
265 .and_then(|permission| permission.custom().cloned()),
266 RoleName::try_from("deployer".to_owned()).ok()
267 );
268 assert_eq!(Permission::from_role_name(""), None);
269 }
270}