use std::{borrow::Cow, fmt, str::FromStr};
use const_macros::const_early;
use miette::Diagnostic;
#[cfg(feature = "serde")]
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
use crate::{
auth::{url, utf8},
macros::errors,
};
pub const SEPARATOR: &str = ":";
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Part<'p> {
string: Cow<'p, str>,
}
#[cfg(feature = "serde")]
impl Serialize for Part<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.as_str().serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Part<'_> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let string = Cow::deserialize(deserializer)?;
Self::new(string).map_err(de::Error::custom)
}
}
#[derive(Debug, Error, Diagnostic)]
#[error("the part is empty")]
#[diagnostic(
code(otp_std::auth::part::empty),
help("make sure the part is not empty")
)]
pub struct EmptyError;
#[derive(Debug, Error, Diagnostic)]
#[error("unexpected `{SEPARATOR}` in `{string}`")]
#[diagnostic(
code(otp_std::auth::part::separator),
help("make sure the part does not contain `{SEPARATOR}`")
)]
pub struct SeparatorError {
pub string: String,
}
impl SeparatorError {
pub const fn new(string: String) -> Self {
Self { string }
}
}
#[derive(Debug, Error, Diagnostic)]
#[error(transparent)]
#[diagnostic(transparent)]
pub enum ErrorSource {
Empty(#[from] EmptyError),
Separator(#[from] SeparatorError),
}
#[derive(Debug, Error, Diagnostic)]
#[error("failed to parse part")]
#[diagnostic(code(otp_std::auth::part), help("see the report for more information"))]
pub struct Error {
#[source]
#[diagnostic_source]
pub source: ErrorSource,
}
impl Error {
pub const fn new(source: ErrorSource) -> Self {
Self { source }
}
pub fn empty(error: EmptyError) -> Self {
Self::new(error.into())
}
pub fn separator(error: SeparatorError) -> Self {
Self::new(error.into())
}
pub fn new_empty() -> Self {
Self::empty(EmptyError)
}
pub fn new_separator(string: String) -> Self {
Self::separator(SeparatorError::new(string))
}
}
impl AsRef<str> for Part<'_> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Part<'_> {
pub fn as_str(&self) -> &str {
self.string.as_ref()
}
}
impl fmt::Display for Part<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_str().fmt(formatter)
}
}
impl FromStr for Part<'_> {
type Err = Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
Self::check(string)?;
Ok(unsafe { Self::owned_unchecked(string.to_owned()) })
}
}
errors! {
Type = Error,
Hack = $,
empty_error => new_empty(),
separator_error => new_separator(string => to_owned),
}
impl<'p> Part<'p> {
pub fn new(string: Cow<'p, str>) -> Result<Self, Error> {
Self::check(string.as_ref())?;
Ok(unsafe { Self::new_unchecked(string) })
}
pub fn check<S: AsRef<str>>(string: S) -> Result<(), Error> {
fn check_inner(string: &str) -> Result<(), Error> {
const_early!(string.is_empty() => empty_error!());
const_early!(string.contains(SEPARATOR) => separator_error!(string));
Ok(())
}
check_inner(string.as_ref())
}
pub const unsafe fn new_unchecked(string: Cow<'p, str>) -> Self {
Self { string }
}
pub fn owned(string: String) -> Result<Self, Error> {
Self::new(Cow::Owned(string))
}
pub const unsafe fn owned_unchecked(string: String) -> Self {
unsafe { Self::new_unchecked(Cow::Owned(string)) }
}
pub fn borrowed(string: &'p str) -> Result<Self, Error> {
Self::new(Cow::Borrowed(string))
}
pub const unsafe fn borrowed_unchecked(string: &'p str) -> Self {
unsafe { Self::new_unchecked(Cow::Borrowed(string)) }
}
pub fn get(self) -> Cow<'p, str> {
self.string
}
}
#[derive(Debug, Error, Diagnostic)]
#[error(transparent)]
#[diagnostic(transparent)]
pub enum DecodeErrorSource {
Utf8(#[from] utf8::Error),
Part(#[from] Error),
}
#[derive(Debug, Error, Diagnostic)]
#[error("failed to decode part")]
#[diagnostic(
code(otp_std::auth::part::decode),
help("see the report for more information")
)]
pub struct DecodeError {
#[source]
#[diagnostic_source]
pub source: DecodeErrorSource,
}
impl DecodeError {
pub const fn new(source: DecodeErrorSource) -> Self {
Self { source }
}
pub fn utf8(error: utf8::Error) -> Self {
Self::new(error.into())
}
pub fn part(error: Error) -> Self {
Self::new(error.into())
}
}
impl Part<'_> {
pub fn decode<S: AsRef<str>>(string: S) -> Result<Self, DecodeError> {
let decoded = url::decode(string.as_ref())
.map_err(utf8::wrap)
.map_err(DecodeError::utf8)?;
Self::owned(decoded.into_owned()).map_err(DecodeError::part)
}
}
impl Part<'_> {
pub fn encode(&self) -> Cow<'_, str> {
url::encode(self.as_str())
}
}
pub type Owned = Part<'static>;
impl Part<'_> {
pub fn into_owned(self) -> Owned {
unsafe { Owned::owned_unchecked(self.get().into_owned()) }
}
}