use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(into = "String", try_from = "String")]
pub enum Assignee {
User(String),
Group(String),
}
impl Assignee {
pub fn user(name: impl Into<String>) -> Self {
Assignee::User(name.into())
}
pub fn group(name: impl Into<String>) -> Self {
Assignee::Group(name.into())
}
pub fn name(&self) -> &str {
match self {
Assignee::User(name) | Assignee::Group(name) => name,
}
}
pub fn is_user(&self) -> bool {
matches!(self, Assignee::User(_))
}
pub fn is_group(&self) -> bool {
matches!(self, Assignee::Group(_))
}
}
impl fmt::Display for Assignee {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Assignee::User(name) => write!(f, "user:{name}"),
Assignee::Group(name) => write!(f, "group:{name}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum AssigneeParseError {
#[error("assignee must be prefixed with `user:` or `group:`, got `{0}`")]
MissingPrefix(String),
#[error("assignee name must not be empty")]
EmptyName,
}
impl FromStr for Assignee {
type Err = AssigneeParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let assignee = if let Some(name) = s.strip_prefix("user:") {
Assignee::User(name.to_string())
} else if let Some(name) = s.strip_prefix("group:") {
Assignee::Group(name.to_string())
} else {
return Err(AssigneeParseError::MissingPrefix(s.to_string()));
};
if assignee.name().is_empty() {
return Err(AssigneeParseError::EmptyName);
}
Ok(assignee)
}
}
impl From<Assignee> for String {
fn from(assignee: Assignee) -> Self {
assignee.to_string()
}
}
impl TryFrom<String> for Assignee {
type Error = AssigneeParseError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn user_and_group_render_with_prefix() {
assert_eq!(Assignee::user("alice").to_string(), "user:alice");
assert_eq!(Assignee::group("sre").to_string(), "group:sre");
}
#[test]
fn round_trips_through_string() {
for value in [Assignee::user("alice"), Assignee::group("sre-oncall")] {
let text = value.to_string();
let parsed: Assignee = text.parse().expect("parse");
assert_eq!(parsed, value);
}
}
#[test]
fn round_trips_through_json() {
let value = Assignee::group("sre-oncall");
let json = serde_json::to_string(&value).expect("serialize");
assert_eq!(json, "\"group:sre-oncall\"");
let back: Assignee = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, value);
}
#[test]
fn missing_prefix_is_rejected() {
assert_eq!(
"sre-oncall".parse::<Assignee>(),
Err(AssigneeParseError::MissingPrefix("sre-oncall".to_string()))
);
}
#[test]
fn empty_name_is_rejected() {
assert_eq!(
"user:".parse::<Assignee>(),
Err(AssigneeParseError::EmptyName)
);
assert_eq!(
"group:".parse::<Assignee>(),
Err(AssigneeParseError::EmptyName)
);
}
#[test]
fn deserialize_rejects_unprefixed() {
assert!(serde_json::from_str::<Assignee>("\"sre-oncall\"").is_err());
}
#[test]
fn name_strips_the_prefix() {
assert_eq!(Assignee::user("alice").name(), "alice");
assert_eq!(Assignee::group("sre").name(), "sre");
}
}