ironflow_store/entities/
assignee.rs1use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
38#[serde(into = "String", try_from = "String")]
39pub enum Assignee {
40 User(String),
42 Group(String),
44}
45
46impl Assignee {
47 pub fn user(name: impl Into<String>) -> Self {
57 Assignee::User(name.into())
58 }
59
60 pub fn group(name: impl Into<String>) -> Self {
70 Assignee::Group(name.into())
71 }
72
73 pub fn name(&self) -> &str {
83 match self {
84 Assignee::User(name) | Assignee::Group(name) => name,
85 }
86 }
87
88 pub fn is_user(&self) -> bool {
99 matches!(self, Assignee::User(_))
100 }
101
102 pub fn is_group(&self) -> bool {
113 matches!(self, Assignee::Group(_))
114 }
115}
116
117impl fmt::Display for Assignee {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 match self {
120 Assignee::User(name) => write!(f, "user:{name}"),
121 Assignee::Group(name) => write!(f, "group:{name}"),
122 }
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Error)]
128pub enum AssigneeParseError {
129 #[error("assignee must be prefixed with `user:` or `group:`, got `{0}`")]
131 MissingPrefix(String),
132 #[error("assignee name must not be empty")]
134 EmptyName,
135}
136
137impl FromStr for Assignee {
138 type Err = AssigneeParseError;
139
140 fn from_str(s: &str) -> Result<Self, Self::Err> {
141 let assignee = if let Some(name) = s.strip_prefix("user:") {
142 Assignee::User(name.to_string())
143 } else if let Some(name) = s.strip_prefix("group:") {
144 Assignee::Group(name.to_string())
145 } else {
146 return Err(AssigneeParseError::MissingPrefix(s.to_string()));
147 };
148
149 if assignee.name().is_empty() {
150 return Err(AssigneeParseError::EmptyName);
151 }
152
153 Ok(assignee)
154 }
155}
156
157impl From<Assignee> for String {
158 fn from(assignee: Assignee) -> Self {
159 assignee.to_string()
160 }
161}
162
163impl TryFrom<String> for Assignee {
164 type Error = AssigneeParseError;
165
166 fn try_from(value: String) -> Result<Self, Self::Error> {
167 value.parse()
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn user_and_group_render_with_prefix() {
177 assert_eq!(Assignee::user("alice").to_string(), "user:alice");
178 assert_eq!(Assignee::group("sre").to_string(), "group:sre");
179 }
180
181 #[test]
182 fn round_trips_through_string() {
183 for value in [Assignee::user("alice"), Assignee::group("sre-oncall")] {
184 let text = value.to_string();
185 let parsed: Assignee = text.parse().expect("parse");
186 assert_eq!(parsed, value);
187 }
188 }
189
190 #[test]
191 fn round_trips_through_json() {
192 let value = Assignee::group("sre-oncall");
193 let json = serde_json::to_string(&value).expect("serialize");
194 assert_eq!(json, "\"group:sre-oncall\"");
195 let back: Assignee = serde_json::from_str(&json).expect("deserialize");
196 assert_eq!(back, value);
197 }
198
199 #[test]
200 fn missing_prefix_is_rejected() {
201 assert_eq!(
202 "sre-oncall".parse::<Assignee>(),
203 Err(AssigneeParseError::MissingPrefix("sre-oncall".to_string()))
204 );
205 }
206
207 #[test]
208 fn empty_name_is_rejected() {
209 assert_eq!(
210 "user:".parse::<Assignee>(),
211 Err(AssigneeParseError::EmptyName)
212 );
213 assert_eq!(
214 "group:".parse::<Assignee>(),
215 Err(AssigneeParseError::EmptyName)
216 );
217 }
218
219 #[test]
220 fn deserialize_rejects_unprefixed() {
221 assert!(serde_json::from_str::<Assignee>("\"sre-oncall\"").is_err());
222 }
223
224 #[test]
225 fn name_strips_the_prefix() {
226 assert_eq!(Assignee::user("alice").name(), "alice");
227 assert_eq!(Assignee::group("sre").name(), "sre");
228 }
229}