1use crate::{Id, ValidationError};
14use std::fmt;
15use std::str::FromStr;
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33pub struct JmapUri {
34 data_type: String,
35 account_id: Id,
36 object_id: Id,
37}
38
39#[non_exhaustive]
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum JmapUriError {
43 NotJmapScheme,
45 InvalidDataType,
47 InvalidAccountId(Option<ValidationError>),
49 MissingObjectId,
51 InvalidObjectId(ValidationError),
54}
55
56impl fmt::Display for JmapUriError {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 match self {
59 Self::NotJmapScheme => f.write_str("URI scheme is not `jmap`"),
60 Self::InvalidDataType => {
61 f.write_str("jmap: URI type is not a lowercased JMAP data type name")
62 }
63 Self::InvalidAccountId(_) => f.write_str("jmap: URI accountId is missing or invalid"),
64 Self::MissingObjectId => f.write_str("jmap: URI has no objectId"),
65 Self::InvalidObjectId(_) => f.write_str("jmap: URI objectId is invalid"),
66 }
67 }
68}
69
70impl std::error::Error for JmapUriError {
71 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
72 match self {
73 Self::InvalidAccountId(Some(e)) | Self::InvalidObjectId(e) => Some(e),
74 _ => None,
75 }
76 }
77}
78
79impl JmapUri {
80 pub fn new(
89 data_type: impl Into<String>,
90 account_id: Id,
91 object_id: Id,
92 ) -> Result<Self, JmapUriError> {
93 let data_type = data_type.into();
94 validate_data_type(&data_type)?;
95 Id::new_validated(account_id.as_ref())
96 .map_err(|e| JmapUriError::InvalidAccountId(Some(e)))?;
97 Id::new_validated(object_id.as_ref()).map_err(JmapUriError::InvalidObjectId)?;
98 Ok(Self {
99 data_type,
100 account_id,
101 object_id,
102 })
103 }
104
105 pub fn data_type(&self) -> &str {
107 &self.data_type
108 }
109
110 pub fn account_id(&self) -> &Id {
112 &self.account_id
113 }
114
115 pub fn object_id(&self) -> &Id {
117 &self.object_id
118 }
119}
120
121fn validate_data_type(s: &str) -> Result<(), JmapUriError> {
122 if !s.is_empty()
123 && s.bytes()
124 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
125 {
126 Ok(())
127 } else {
128 Err(JmapUriError::InvalidDataType)
129 }
130}
131
132impl FromStr for JmapUri {
133 type Err = JmapUriError;
134
135 fn from_str(s: &str) -> Result<Self, Self::Err> {
138 let mut parts = s.splitn(4, ':');
141 let scheme = parts.next().unwrap_or_default();
142 if !scheme.eq_ignore_ascii_case("jmap") {
143 return Err(JmapUriError::NotJmapScheme);
144 }
145 let data_type = parts.next().ok_or(JmapUriError::InvalidDataType)?;
146 validate_data_type(data_type)?;
147 let account_id = parts.next().ok_or(JmapUriError::InvalidAccountId(None))?;
148 let account_id =
149 Id::new_validated(account_id).map_err(|e| JmapUriError::InvalidAccountId(Some(e)))?;
150 let object_id = parts.next().ok_or(JmapUriError::MissingObjectId)?;
151 let object_id = Id::new_validated(object_id).map_err(JmapUriError::InvalidObjectId)?;
152 Self::new(data_type, account_id, object_id)
153 }
154}
155
156impl fmt::Display for JmapUri {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 write!(
159 f,
160 "jmap:{}:{}:{}",
161 self.data_type, self.account_id, self.object_id
162 )
163 }
164}