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