use compose_spec_macros::{DeserializeTryFromString, SerializeDisplay};
use thiserror::Error;
#[derive(
SerializeDisplay, DeserializeTryFromString, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
)]
pub struct Identifier(Box<str>);
impl Identifier {
pub fn new<T>(identifier: T) -> Result<Self, InvalidIdentifierError>
where
T: AsRef<str> + Into<Box<str>>,
{
let mut chars = identifier.as_ref().chars();
let first = chars.next().ok_or(InvalidIdentifierError::Empty)?;
if !first.is_ascii_alphanumeric() {
return Err(InvalidIdentifierError::Start(first));
}
for char in chars {
if !(char.is_ascii_alphanumeric() || matches!(char, '.' | '_' | '-')) {
return Err(InvalidIdentifierError::Character(char));
}
}
Ok(Self(identifier.into()))
}
}
#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
pub enum InvalidIdentifierError {
#[error("identifier cannot be empty")]
Empty,
#[error(
"invalid start character `{0}`, identifiers must start with an ASCII letter (a-z, A-Z) \
or digit (0-9)"
)]
Start(char),
#[error(
"invalid character `{0}`, identifiers must contain only ASCII letters (a-z, A-Z), \
digits (0-9), dots (.), underscores (_), or dashes (-)"
)]
Character(char),
}
#[derive(
SerializeDisplay, DeserializeTryFromString, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
)]
pub struct MapKey(Box<str>);
impl MapKey {
pub fn new<T>(key: T) -> Result<Self, InvalidMapKeyError>
where
T: AsRef<str> + Into<Box<str>>,
{
let key_str = key.as_ref();
if key_str.is_empty() {
Err(InvalidMapKeyError::Empty)
} else if key_str.contains('\n') {
Err(InvalidMapKeyError::MultipleLines)
} else {
Ok(Self(key.into()))
}
}
}
#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
pub enum InvalidMapKeyError {
#[error("map key cannot be empty")]
Empty,
#[error("map key cannot have multiple lines (newline character `\\n` found)")]
MultipleLines,
}
#[derive(
SerializeDisplay, DeserializeTryFromString, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
)]
pub struct ExtensionKey(Box<str>);
impl ExtensionKey {
pub fn new<T>(key: T) -> Result<Self, InvalidExtensionKeyError>
where
T: AsRef<str> + Into<Box<str>>,
{
let key_str = key.as_ref();
if !key_str.starts_with("x-") {
Err(InvalidExtensionKeyError::MissingPrefix(key.into()))
} else if key_str.contains('\n') {
Err(InvalidExtensionKeyError::MultipleLines)
} else {
Ok(Self(key.into()))
}
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn strip_prefix(&self) -> &str {
self.as_str()
.strip_prefix("x-")
.expect("`ExtensionKey`s always start with \"x-\"")
}
}
#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum InvalidExtensionKeyError {
#[error("extension key `{0}` does not start with \"x-\"")]
MissingPrefix(Box<str>),
#[error("map key cannot have multiple lines (newline character `\\n` found)")]
MultipleLines,
}
macro_rules! key_impls {
($($Ty:ident => $Error:ty),* $(,)?) => {
$(
impl $Ty {
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_ref()
}
}
crate::impl_try_from! {
$Ty::new -> $Error,
String,
Box<str>,
&str,
::std::borrow::Cow<'_, str>,
}
impl ::std::str::FromStr for $Ty {
type Err = $Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.try_into()
}
}
impl AsRef<str> for $Ty {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl ::std::borrow::Borrow<str> for $Ty {
fn borrow(&self) -> &str {
self.as_str()
}
}
impl ::std::cmp::PartialEq<str> for $Ty {
fn eq(&self, other: &str) -> bool {
self.as_str().eq(other)
}
}
impl ::std::cmp::PartialEq<&str> for $Ty {
fn eq(&self, other: &&str) -> bool {
self.as_str().eq(*other)
}
}
impl ::std::fmt::Display for $Ty {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
f.write_str(self.as_str())
}
}
impl From<$Ty> for Box<str> {
fn from(value: $Ty) -> Self {
value.0.into()
}
}
impl From<$Ty> for String {
fn from(value: $Ty) -> Self {
value.0.into()
}
}
)*
};
}
pub(crate) use key_impls;
key_impls! {
Identifier => InvalidIdentifierError,
MapKey => InvalidMapKeyError,
ExtensionKey => InvalidExtensionKeyError,
}
#[cfg(test)]
mod tests {
use proptest::{
arbitrary::Arbitrary,
strategy::{BoxedStrategy, Strategy},
};
use super::*;
impl Arbitrary for Identifier {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
"[a-zA-Z0-9][a-zA-Z0-9._-]*"
.prop_map_into()
.prop_map(Self)
.boxed()
}
}
}