use std::{fmt, str::FromStr};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
use thiserror::Error;
use crate::MAX_IDENTIFIER_LENGTH;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum SubjectService {
Slack,
Telegram,
Tel,
}
impl SubjectService {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Slack => "slack",
Self::Telegram => "telegram",
Self::Tel => "tel",
}
}
const fn requires_tenant(self) -> bool {
matches!(self, Self::Slack)
}
}
impl FromStr for SubjectService {
type Err = SubjectError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"slack" => Ok(Self::Slack),
"telegram" => Ok(Self::Telegram),
"tel" => Ok(Self::Tel),
_ => Err(SubjectError::UnknownService {
service: value.to_owned(),
}),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ExternalSubject {
service: SubjectService,
tenant: Option<String>,
subject: String,
}
impl ExternalSubject {
pub fn slack(team: &str, user: &str) -> Result<Self, SubjectError> {
let tenant = normalize_segment(team, "tenant")?;
let subject = normalize_segment(user, "subject")?;
Self::build(SubjectService::Slack, Some(tenant), subject)
}
pub fn telegram(user: &str) -> Result<Self, SubjectError> {
let subject = normalize_segment(user, "subject")?;
Self::build(SubjectService::Telegram, None, subject)
}
pub fn telephone(number: &str) -> Result<Self, SubjectError> {
let digits = number.strip_prefix('+').unwrap_or(number);
if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(SubjectError::InvalidSegment {
segment: "subject",
value: number.to_owned(),
});
}
Self::build(SubjectService::Tel, None, digits.to_owned())
}
fn build(
service: SubjectService,
tenant: Option<String>,
subject: String,
) -> Result<Self, SubjectError> {
let candidate = Self {
service,
tenant,
subject,
};
if candidate.canonical().len() > MAX_IDENTIFIER_LENGTH {
return Err(SubjectError::TooLong {
maximum: MAX_IDENTIFIER_LENGTH,
});
}
Ok(candidate)
}
#[must_use]
pub fn service(&self) -> SubjectService {
self.service
}
#[must_use]
pub fn tenant(&self) -> Option<&str> {
self.tenant.as_deref()
}
#[must_use]
pub fn subject(&self) -> &str {
&self.subject
}
#[must_use]
pub fn canonical(&self) -> String {
match &self.tenant {
Some(tenant) => format!("{}.{tenant}.{}", self.service.as_str(), self.subject),
None => format!("{}.{}", self.service.as_str(), self.subject),
}
}
#[must_use]
pub fn in_namespace(&self, scope: &str) -> bool {
let canonical = self.canonical();
canonical == scope
|| (canonical.len() > scope.len()
&& canonical.starts_with(scope)
&& canonical.as_bytes()[scope.len()] == b'.')
}
}
impl fmt::Display for ExternalSubject {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.canonical())
}
}
impl FromStr for ExternalSubject {
type Err = SubjectError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let mut segments = value.split('.');
let service = segments
.next()
.filter(|segment| !segment.is_empty())
.ok_or(SubjectError::Empty)?
.parse::<SubjectService>()?;
let second = segments
.next()
.ok_or(SubjectError::MissingSegment { segment: "subject" })?;
let third = segments.next();
if segments.next().is_some() {
return Err(SubjectError::TooManySegments);
}
let (tenant, subject) = if service.requires_tenant() {
let subject = third.ok_or(SubjectError::MissingSegment { segment: "subject" })?;
(Some(second), subject)
} else {
if third.is_some() {
return Err(SubjectError::TooManySegments);
}
(None, second)
};
let tenant = tenant
.map(|tenant| require_canonical_segment(tenant, "tenant"))
.transpose()?;
let subject = require_canonical_segment(subject, "subject")?;
if service == SubjectService::Tel && !subject.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(SubjectError::InvalidSegment {
segment: "subject",
value: subject,
});
}
Self::build(service, tenant, subject)
}
}
impl Serialize for ExternalSubject {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.canonical())
}
}
impl<'de> Deserialize<'de> for ExternalSubject {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
String::deserialize(deserializer)?
.parse()
.map_err(D::Error::custom)
}
}
fn normalize_segment(value: &str, segment: &'static str) -> Result<String, SubjectError> {
let normalized = value.to_ascii_lowercase();
require_canonical_segment(&normalized, segment)
}
fn require_canonical_segment(
value: impl Into<String>,
segment: &'static str,
) -> Result<String, SubjectError> {
let value = value.into();
if value.is_empty()
|| !value
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
{
return Err(SubjectError::InvalidSegment { segment, value });
}
Ok(value)
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
pub enum SubjectError {
#[error("external subject must not be empty")]
Empty,
#[error("unknown external subject service {service:?}")]
UnknownService {
service: String,
},
#[error("external subject is missing its {segment} segment")]
MissingSegment {
segment: &'static str,
},
#[error("external subject has more segments than its service defines")]
TooManySegments,
#[error("external subject {segment} segment {value:?} is not canonical")]
InvalidSegment {
segment: &'static str,
value: String,
},
#[error("external subject exceeds {maximum} bytes")]
TooLong {
maximum: usize,
},
}
#[cfg(test)]
mod tests {
use super::{ExternalSubject, SubjectService};
#[test]
fn raw_transport_identifiers_normalize_into_canonical_form() {
let slack = ExternalSubject::slack("T0123ABC", "U9XYZ").expect("slack subject");
assert_eq!(slack.canonical(), "slack.t0123abc.u9xyz");
assert_eq!(slack.service(), SubjectService::Slack);
assert_eq!(slack.tenant(), Some("t0123abc"));
let tel = ExternalSubject::telephone("+16034700182").expect("telephone subject");
assert_eq!(tel.canonical(), "tel.16034700182");
assert_eq!(tel.tenant(), None);
let telegram = ExternalSubject::telegram("5551234").expect("telegram subject");
assert_eq!(telegram.canonical(), "telegram.5551234");
}
#[test]
fn canonical_forms_round_trip_through_parse_and_serde() {
for canonical in [
"slack.t0123abc.u9xyz",
"telegram.5551234",
"tel.16034700182",
] {
let subject = canonical
.parse::<ExternalSubject>()
.expect("canonical form parses");
assert_eq!(subject.canonical(), canonical);
let encoded = serde_json::to_string(&subject).expect("serializes");
assert_eq!(encoded, format!("{canonical:?}"));
let decoded = serde_json::from_str::<ExternalSubject>(&encoded).expect("deserializes");
assert_eq!(decoded, subject);
}
}
#[test]
fn canonical_subjects_satisfy_the_identifier_grammar() {
for canonical in [
"slack.t0123abc.u9xyz",
"telegram.5551234",
"tel.16034700182",
] {
canonical
.parse::<crate::PrincipalId>()
.expect("canonical subjects fit the identifier grammar");
}
}
#[test]
fn malformed_subjects_fail_closed() {
for invalid in [
"",
"slack",
"slack.t0123abc",
"slack.t0123abc.u9xyz.extra",
"telegram.5551234.extra",
"tel.not-digits",
"tel.+1603",
"sms.5551234",
"slack..u9xyz",
"slack.T0123.u9",
] {
assert!(
invalid.parse::<ExternalSubject>().is_err(),
"{invalid:?} must not parse"
);
}
assert!(ExternalSubject::slack("team space", "user").is_err());
assert!(ExternalSubject::telephone("call-me").is_err());
}
#[test]
fn namespace_scopes_match_on_segment_boundaries_only() {
let subject = "slack.t0123abc.u9xyz"
.parse::<ExternalSubject>()
.expect("canonical form parses");
assert!(subject.in_namespace("slack"));
assert!(subject.in_namespace("slack.t0123abc"));
assert!(subject.in_namespace("slack.t0123abc.u9xyz"));
assert!(!subject.in_namespace("slack.t0123abcx"));
assert!(!subject.in_namespace("slack.t0123ab"));
assert!(!subject.in_namespace("tel"));
}
}