use crate::{Id, ValidationError};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct JmapUri {
data_type: String,
account_id: Id,
object_id: Id,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum JmapUriError {
NotJmapScheme,
InvalidDataType,
InvalidAccountId(Option<ValidationError>),
MissingObjectId,
InvalidObjectId(ValidationError),
}
impl fmt::Display for JmapUriError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotJmapScheme => f.write_str("URI scheme is not `jmap`"),
Self::InvalidDataType => {
f.write_str("jmap: URI type is not a lowercased JMAP data type name")
}
Self::InvalidAccountId(_) => f.write_str("jmap: URI accountId is missing or invalid"),
Self::MissingObjectId => f.write_str("jmap: URI has no objectId"),
Self::InvalidObjectId(_) => f.write_str("jmap: URI objectId is invalid"),
}
}
}
impl std::error::Error for JmapUriError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::InvalidAccountId(Some(e)) | Self::InvalidObjectId(e) => Some(e),
_ => None,
}
}
}
impl JmapUri {
pub fn new(
data_type: impl Into<String>,
account_id: Id,
object_id: Id,
) -> Result<Self, JmapUriError> {
let data_type = data_type.into();
validate_data_type(&data_type)?;
Id::new_validated(account_id.as_ref())
.map_err(|e| JmapUriError::InvalidAccountId(Some(e)))?;
Id::new_validated(object_id.as_ref()).map_err(JmapUriError::InvalidObjectId)?;
Ok(Self {
data_type,
account_id,
object_id,
})
}
pub fn data_type(&self) -> &str {
&self.data_type
}
pub fn account_id(&self) -> &Id {
&self.account_id
}
pub fn object_id(&self) -> &Id {
&self.object_id
}
}
fn validate_data_type(s: &str) -> Result<(), JmapUriError> {
if !s.is_empty()
&& s.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
{
Ok(())
} else {
Err(JmapUriError::InvalidDataType)
}
}
impl FromStr for JmapUri {
type Err = JmapUriError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.splitn(4, ':');
let scheme = parts.next().unwrap_or_default();
if !scheme.eq_ignore_ascii_case("jmap") {
return Err(JmapUriError::NotJmapScheme);
}
let data_type = parts.next().ok_or(JmapUriError::InvalidDataType)?;
validate_data_type(data_type)?;
let account_id = parts.next().ok_or(JmapUriError::InvalidAccountId(None))?;
let account_id =
Id::new_validated(account_id).map_err(|e| JmapUriError::InvalidAccountId(Some(e)))?;
let object_id = parts.next().ok_or(JmapUriError::MissingObjectId)?;
let object_id = Id::new_validated(object_id).map_err(JmapUriError::InvalidObjectId)?;
Self::new(data_type, account_id, object_id)
}
}
impl fmt::Display for JmapUri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"jmap:{}:{}:{}",
self.data_type, self.account_id, self.object_id
)
}
}