Skip to main content

jmap_types/
jmap_uri.rs

1//! The `jmap:` JMAP object URI
2//! (draft-atwood-jmap-object-uri-00 §JMAP Object URIs).
3//!
4//! The draft defines the syntax as
5//! `jmap-object-uri = "jmap:" type-name ":" account-id ":" object-id`,
6//! where `type-name` is `1*( %x61-7A / DIGIT )` and `account-id` and
7//! `object-id` are `1*255id-char`, the RFC 8620 §1.2 `Id` alphabet. That
8//! alphabet excludes `:`, so splitting on `:` is unambiguous, and a URI
9//! with any further segment matches no form this crate implements
10//! (§Other Forms: "A receiver treats a `jmap` URI that matches no form it
11//! implements as unresolvable").
12
13use crate::{Id, ValidationError};
14use std::fmt;
15use std::str::FromStr;
16
17/// A parsed `jmap:<type>:<accountId>:<objectId>` URI.
18///
19/// Build one with [`JmapUri::new`] or parse one with [`str::parse`];
20/// [`Display`](fmt::Display) renders it back to the wire string. The URI
21/// arrives in peer-supplied values, so parsing validates every segment.
22///
23/// ```rust
24/// use jmap_types::JmapUri;
25///
26/// let uri: JmapUri = "jmap:calendarevent:a1:e42".parse().unwrap();
27/// assert_eq!(uri.data_type(), "calendarevent");
28/// assert_eq!(uri.account_id().as_ref(), "a1");
29/// assert_eq!(uri.object_id().as_ref(), "e42");
30/// assert_eq!(uri.to_string(), "jmap:calendarevent:a1:e42");
31/// ```
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33pub struct JmapUri {
34    data_type: String,
35    account_id: Id,
36    object_id: Id,
37}
38
39/// Why a string or set of parts is not a valid [`JmapUri`].
40#[non_exhaustive]
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum JmapUriError {
43    /// The URI scheme is not `jmap`.
44    NotJmapScheme,
45    /// The `<type>` segment is empty or not lowercase ASCII alphanumeric.
46    InvalidDataType,
47    /// The `<accountId>` segment is missing or not a valid `Id`.
48    InvalidAccountId(Option<ValidationError>),
49    /// No `<objectId>` segment follows the account.
50    MissingObjectId,
51    /// The `<objectId>` segment is not a valid `Id`, including when
52    /// further `:` segments follow it.
53    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    /// Build a URI from its parts.
81    ///
82    /// # Errors
83    ///
84    /// Returns [`JmapUriError`] when `data_type` is not lowercase ASCII
85    /// alphanumeric or either id fails RFC 8620 §1.2 validation (an
86    /// unvalidated [`Id::from`] could otherwise smuggle a `:` into the
87    /// rendered URI).
88    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    /// The lowercased type name, e.g. `calendarevent`.
106    pub fn data_type(&self) -> &str {
107        &self.data_type
108    }
109
110    /// The account the object lives on.
111    pub fn account_id(&self) -> &Id {
112        &self.account_id
113    }
114
115    /// The referenced object's `id`.
116    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    /// Parse a `jmap:` URI. The scheme matches case-insensitively
136    /// (RFC 3986 §3.1); every other segment is case-sensitive.
137    fn from_str(s: &str) -> Result<Self, Self::Err> {
138        // At most four parts: anything after the object-id stays in it and
139        // fails `Id` validation, since `:` is not an `id-char`.
140        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}