use std::{borrow::Borrow, collections::HashMap, error::Error, fmt, str::FromStr};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use crate::{Categorized, Category, Failure, Permanent};
#[cfg(test)]
mod tests;
const SUBJECT_CHARACTERS: &str = "A-Z, a-z, 0-9, underscores, and hyphens";
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Subject(String);
impl Subject {
#[must_use]
#[track_caller]
pub fn new(subject: &str) -> Self {
match subject.parse() {
Ok(subject) => subject,
Err(refusal) => panic!("architecturekit: {refusal}"),
}
}
#[must_use]
pub fn root() -> Self {
Self("/".to_owned())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for Subject {
type Err = NotASubject;
fn from_str(subject: &str) -> Result<Self, NotASubject> {
let refused = |reason| NotASubject {
subject: subject.to_owned(),
reason,
};
let Some(rest) = subject.strip_prefix('/') else {
return Err(refused(Reason::NoLeadingSlash));
};
if rest.is_empty() {
return Ok(Self::root());
}
let rest = rest.strip_suffix('/').unwrap_or(rest);
for segment in rest.split('/') {
if segment.is_empty() {
return Err(refused(Reason::EmptySegment));
}
if !has_only_subject_characters(segment) {
return Err(refused(Reason::Characters));
}
}
Ok(Self(format!("/{rest}")))
}
}
impl fmt::Display for Subject {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.pad(&self.0)
}
}
impl AsRef<str> for Subject {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for Subject {
fn borrow(&self) -> &str {
&self.0
}
}
impl From<Subject> for String {
fn from(subject: Subject) -> Self {
subject.0
}
}
impl Serialize for Subject {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for Subject {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(SubjectVisitor)
}
}
struct SubjectVisitor;
impl de::Visitor<'_> for SubjectVisitor {
type Value = Subject;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a subject, which is a string such as \"/books/42\"")
}
fn visit_str<E: de::Error>(self, subject: &str) -> Result<Subject, E> {
subject.parse().map_err(E::custom)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NotASubject {
subject: String,
reason: Reason,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Reason {
NoLeadingSlash,
EmptySegment,
Characters,
}
impl NotASubject {
#[must_use]
pub fn subject(&self) -> &str {
&self.subject
}
}
impl fmt::Display for NotASubject {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "not a subject: {:?}, ", self.subject)?;
match self.reason {
Reason::NoLeadingSlash => formatter.write_str("which does not start with a slash"),
Reason::EmptySegment => formatter.write_str("which has an empty segment"),
Reason::Characters => write!(
formatter,
"whose segments may only contain {SUBJECT_CHARACTERS}"
),
}
}
}
impl Error for NotASubject {}
impl Categorized for NotASubject {
fn category(&self) -> Option<Category> {
None
}
}
pub(crate) fn subject_of(event: &eventsourcingdb::Event) -> Result<Subject, Permanent> {
event.subject().parse().map_err(|refusal: NotASubject| {
Permanent::Other(Failure::new(
format!(
"event {:?} is on {:?}, which is not a subject",
event.id(),
event.subject(),
),
Some(Box::new(refusal)),
))
})
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SubjectScheme {
pattern: String,
segments: Vec<Segment>,
placeholders: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum Segment {
Literal(String),
Placeholder,
}
impl SubjectScheme {
#[must_use]
pub fn new(pattern: &str) -> Self {
let Some(rest) = pattern.strip_prefix('/') else {
panic!("architecturekit: subject pattern {pattern:?} must start with a slash");
};
let mut segments = Vec::new();
let mut placeholders: Vec<String> = Vec::new();
for segment in rest.split('/') {
assert!(
!segment.is_empty(),
"architecturekit: subject pattern {pattern:?} has an empty segment",
);
let Some(inner) = segment.strip_prefix('{') else {
assert!(
!segment.contains(['{', '}']),
"architecturekit: segment {segment:?} in {pattern:?} is malformed",
);
assert!(
has_only_subject_characters(segment),
"architecturekit: segment {segment:?} in {pattern:?} may only contain {SUBJECT_CHARACTERS}",
);
segments.push(Segment::Literal(segment.to_owned()));
continue;
};
let name = match inner.strip_suffix('}') {
Some(name) if !name.contains(['{', '}']) => name,
_ => panic!("architecturekit: segment {segment:?} in {pattern:?} is malformed"),
};
assert!(
!name.is_empty(),
"architecturekit: pattern {pattern:?} has an unnamed placeholder",
);
assert!(
has_only_subject_characters(name),
"architecturekit: placeholder {name:?} in {pattern:?} may only contain {SUBJECT_CHARACTERS}",
);
assert!(
!placeholders.iter().any(|existing| existing == name),
"architecturekit: pattern {pattern:?} uses the placeholder {name:?} twice",
);
placeholders.push(name.to_owned());
segments.push(Segment::Placeholder);
}
Self {
pattern: pattern.to_owned(),
segments,
placeholders,
}
}
#[must_use]
pub fn pattern(&self) -> &str {
&self.pattern
}
#[must_use]
pub fn root(&self) -> Subject {
let literals: Vec<&str> = self
.segments
.iter()
.map_while(|segment| match segment {
Segment::Literal(literal) => Some(literal.as_str()),
Segment::Placeholder => None,
})
.collect();
Subject(format!("/{}", literals.join("/")))
}
#[must_use]
pub fn placeholders(&self) -> &[String] {
&self.placeholders
}
#[must_use]
pub fn build<V: AsRef<str>>(&self, values: &[V]) -> Subject {
if let Err(message) = self.check_values(values, true) {
panic!("{message}");
}
self.compose(values)
}
pub fn try_build<V: AsRef<str>>(&self, values: &[V]) -> Result<Subject, SubjectValuesError> {
self.check(values)?;
Ok(self.compose(values))
}
fn compose<V: AsRef<str>>(&self, values: &[V]) -> Subject {
let mut subject = String::new();
let mut next = 0;
for segment in &self.segments {
subject.push('/');
match segment {
Segment::Literal(literal) => subject.push_str(literal),
Segment::Placeholder => {
subject.push_str(values[next].as_ref());
next += 1;
}
}
}
Subject(subject)
}
pub fn check<V: AsRef<str>>(&self, values: &[V]) -> Result<(), SubjectValuesError> {
self.check_values(values, false).map_err(SubjectValuesError)
}
fn check_values<V: AsRef<str>>(
&self,
values: &[V],
is_for_developer: bool,
) -> Result<(), String> {
let (wanted, got) = (self.placeholders.len(), values.len());
if wanted != got {
return Err(if is_for_developer {
format!(
"architecturekit: pattern {:?} needs {wanted} value(s), got {got}",
self.pattern,
)
} else {
format!("the subject needs {wanted} value(s), got {got}")
});
}
let (prefix, in_pattern) = if is_for_developer {
("architecturekit: ", format!(" in {:?}", self.pattern))
} else {
("", String::new())
};
for (value, placeholder) in values.iter().zip(&self.placeholders) {
let value = value.as_ref();
if value.is_empty() {
return Err(format!(
"{prefix}value for {placeholder:?}{in_pattern} must not be empty"
));
}
if !has_only_subject_characters(value) {
return Err(format!(
"{prefix}value {value:?} for {placeholder:?}{in_pattern} may only contain {SUBJECT_CHARACTERS}"
));
}
}
Ok(())
}
#[must_use]
pub fn match_subject<'scheme, 'subject, S>(
&'scheme self,
subject: &'subject S,
) -> Option<HashMap<&'scheme str, &'subject str>>
where
S: AsRef<str> + ?Sized,
{
let parts: Vec<&str> = subject.as_ref().strip_prefix('/')?.split('/').collect();
if parts.len() != self.segments.len() {
return None;
}
let mut placeholders = self.placeholders.iter();
let mut values = Vec::with_capacity(self.placeholders.len());
for (segment, part) in self.segments.iter().zip(parts) {
match segment {
Segment::Literal(literal) => {
if part != literal {
return None;
}
}
Segment::Placeholder => {
if part.is_empty() || !has_only_subject_characters(part) {
return None;
}
values.push((placeholders.next()?.as_str(), part));
}
}
}
Some(values.into_iter().collect())
}
#[must_use]
pub fn value<'subject, S>(
&self,
subject: &'subject S,
placeholder: &str,
) -> Option<&'subject str>
where
S: AsRef<str> + ?Sized,
{
assert!(
self.placeholders.iter().any(|name| name == placeholder),
"architecturekit: pattern {:?} has no placeholder {placeholder:?}",
self.pattern,
);
self.match_subject(subject)
.and_then(|values| values.get(placeholder).copied())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SubjectValuesError(String);
impl fmt::Display for SubjectValuesError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl Error for SubjectValuesError {}
impl Categorized for SubjectValuesError {
fn category(&self) -> Option<Category> {
None
}
}
fn has_only_subject_characters(segment: &str) -> bool {
segment
.chars()
.all(|character| character.is_ascii_alphanumeric() || character == '_' || character == '-')
}