use std::{collections::HashMap, error::Error, fmt};
#[cfg(test)]
mod tests;
const SUBJECT_CHARACTERS: &str = "A-Z, a-z, 0-9, underscores, and hyphens";
#[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) -> String {
let literals: Vec<&str> = self
.segments
.iter()
.map_while(|segment| match segment {
Segment::Literal(literal) => Some(literal.as_str()),
Segment::Placeholder => None,
})
.collect();
format!("/{}", literals.join("/"))
}
#[must_use]
pub fn placeholders(&self) -> &[String] {
&self.placeholders
}
#[must_use]
pub fn build<V: AsRef<str>>(&self, values: &[V]) -> String {
if let Err(message) = self.check_values(values, true) {
panic!("{message}");
}
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
}
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>(
&'scheme self,
subject: &'subject str,
) -> Option<HashMap<&'scheme str, &'subject str>> {
let parts: Vec<&str> = subject.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>(
&self,
subject: &'subject str,
placeholder: &str,
) -> Option<&'subject str> {
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 {}
fn has_only_subject_characters(segment: &str) -> bool {
segment
.chars()
.all(|character| character.is_ascii_alphanumeric() || character == '_' || character == '-')
}