use std::{fmt, str::FromStr};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum InvalidPathTemplate {
#[error("path template `{0}` must begin with `/`")]
MissingLeadingSlash(String),
#[error("path template `{0}` has unbalanced braces")]
UnbalancedBraces(String),
#[error("path template `{0}` contains an empty `{{}}` expression")]
EmptyExpression(String),
#[error("path template `{template}` repeats the variable `{name}`")]
DuplicateVariable {
template: String,
name: String,
},
#[error("path template `{0}` must not contain a query string or fragment")]
NotAPath(String),
#[error(
"path template `{template}` contains `{character}` outside a `{{}}` expression, which the \
path grammar does not allow"
)]
IllegalLiteralCharacter {
template: String,
character: char,
},
#[error("path template `{0}` contains a `%` that does not introduce a percent-encoded triple")]
MalformedPercentEncoding(String),
#[error("path template `{0}` has an empty segment")]
EmptySegment(String),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct PathTemplate {
raw: String,
variables: Vec<String>,
}
const fn is_path_character(character: char) -> bool {
character.is_ascii_alphanumeric()
|| matches!(
character,
'-' | '.'
| '_'
| '~'
| '!'
| '$'
| '&'
| '\''
| '('
| ')'
| '*'
| '+'
| ','
| ';'
| '='
| ':'
| '@'
)
}
fn check_literal(literal: &str, raw: &str) -> Result<(), InvalidPathTemplate> {
let mut characters = literal.chars();
while let Some(character) = characters.next() {
match character {
'/' => {}
'}' => return Err(InvalidPathTemplate::UnbalancedBraces(raw.to_owned())),
'%' => {
let high = characters.next();
let low = characters.next();
if !matches!((high, low), (Some(high), Some(low))
if high.is_ascii_hexdigit() && low.is_ascii_hexdigit())
{
return Err(InvalidPathTemplate::MalformedPercentEncoding(
raw.to_owned(),
));
}
}
_ if is_path_character(character) => {}
_ => {
return Err(InvalidPathTemplate::IllegalLiteralCharacter {
template: raw.to_owned(),
character,
});
}
}
}
Ok(())
}
fn check_segments(raw: &str) -> Result<(), InvalidPathTemplate> {
let mut in_expression = false;
let mut segment_is_empty = true;
for character in raw.chars().skip(1) {
match character {
'{' => {
in_expression = true;
segment_is_empty = false;
}
'}' => in_expression = false,
'/' if !in_expression => {
if segment_is_empty {
return Err(InvalidPathTemplate::EmptySegment(raw.to_owned()));
}
segment_is_empty = true;
}
_ => segment_is_empty = false,
}
}
Ok(())
}
impl PathTemplate {
pub fn parse(raw: impl Into<String>) -> Result<Self, InvalidPathTemplate> {
let raw = raw.into();
if !raw.starts_with('/') {
return Err(InvalidPathTemplate::MissingLeadingSlash(raw));
}
if raw.contains('?') || raw.contains('#') {
return Err(InvalidPathTemplate::NotAPath(raw));
}
let mut variables = Vec::new();
let mut rest = raw.as_str();
while let Some(open) = rest.find('{') {
check_literal(&rest[..open], &raw)?;
let after_open = &rest[open + 1..];
let Some(close) = after_open.find('}') else {
return Err(InvalidPathTemplate::UnbalancedBraces(raw));
};
let name = &after_open[..close];
if name.is_empty() {
return Err(InvalidPathTemplate::EmptyExpression(raw));
}
if name.contains('{') {
return Err(InvalidPathTemplate::UnbalancedBraces(raw));
}
if variables.iter().any(|existing| existing == name) {
return Err(InvalidPathTemplate::DuplicateVariable {
name: name.to_owned(),
template: raw,
});
}
variables.push(name.to_owned());
rest = &after_open[close + 1..];
}
if rest.contains('}') {
return Err(InvalidPathTemplate::UnbalancedBraces(raw));
}
check_literal(rest, &raw)?;
check_segments(&raw)?;
Ok(Self { raw, variables })
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.raw
}
#[must_use]
pub fn variables(&self) -> &[String] {
&self.variables
}
#[must_use]
pub fn normalized(&self) -> String {
let mut out = String::with_capacity(self.raw.len());
let mut rest = self.raw.as_str();
while let Some(open) = rest.find('{') {
out.push_str(&rest[..open]);
out.push_str("{}");
let after_open = &rest[open + 1..];
let close = after_open.find('}').expect("parse validated the braces");
rest = &after_open[close + 1..];
}
out.push_str(rest);
out
}
pub fn with_prefix(&self, prefix: &str) -> Result<Self, InvalidPathTemplate> {
let prefix = prefix.trim_end_matches('/');
if prefix.is_empty() {
return Ok(self.clone());
}
Self::parse(format!("{prefix}{}", self.raw))
}
}
impl fmt::Display for PathTemplate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.raw)
}
}
impl FromStr for PathTemplate {
type Err = InvalidPathTemplate;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::parse(value)
}
}
impl TryFrom<String> for PathTemplate {
type Error = InvalidPathTemplate;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
impl From<PathTemplate> for String {
fn from(template: PathTemplate) -> Self {
template.raw
}
}