use jiff::civil::DateTime; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::{Number, Value}; use std::any::Any; use std::collections::BTreeMap; use std::collections::btree_map::{Iter, Keys}; use std::hash::Hash; // 1.1 Notational Conventions {{{ // Types // *, unknown value #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(untagged)] pub enum JUnknown { Object(BTreeMap), Array(Vec), Uint(JUint), Int(JInt), Number(JNumber), JDate(JDate), String(String), Id(JMAPId), Bool(bool), Null, // Json(serde_json::Value) } /// Implement from for serde_json::Value so that we can use serde to covert unknown to actual types /// :3 impl From for Value { fn from(value: JUnknown) -> Self { match value { JUnknown::Number(n) => { Value::Number(serde_json::Number::from_f64(n).unwrap_or(Number::from(999))) } JUnknown::Int(n) => Value::Number(serde_json::Number::from(n)), JUnknown::Uint(n) => Value::Number(serde_json::Number::from(n)), JUnknown::JDate(jdate) => Value::String(jdate.0.to_string()), JUnknown::Object(btree_map) => { let mut map = serde_json::map::Map::with_capacity(btree_map.len()); for (k, v) in btree_map.into_iter() { let val = v.into(); map.insert(k, val); } Value::Object(map) } JUnknown::Array(junknowns) => { Value::Array(junknowns.into_iter().map(|x| x.into()).collect()) } JUnknown::String(x) => Value::String(x), JUnknown::Id(jmapid) => Value::String(jmapid.0), JUnknown::Bool(bool) => Value::Bool(bool), JUnknown::Null => Value::Null, } } } // JSON String type // pub type JString = String; // JSON Number type pub type JNumber = f64; // JSON Boolean type // pub type JBool = bool; // A[B] is a JSON object where keys are all of type A and values type B; pub type JObj where A: Serialize, B: Serialize, A: DeserializeOwned, B: DeserializeOwned, = BTreeMap; pub type JObject = JObj; /// A[] is an array of type A pub type JArr where A: Serialize, A: DeserializeOwned, = Vec; //}}} // 1.2 The ID data type {{{ /// The ID data type /// Where Id is given as a data type, it means a String of at least 1 and a maximum of 255 octets in size, /// and it MUST only contain characters from the "URL and Filename Safe" base64 alphabet, as defined in Section 5 of [RFC4648], /// excluding the pad character (=). This means the allowed characters are the ASCII alphanumeric characters (A-Za-z0-9), hyphen (-), and underscore (_). /// TODO: Add the restrictions above #[derive(Serialize, Deserialize, Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)] pub struct JMAPId(String); //}}} // 1.3 Int and UnsignedInt {{{ type JInt = i64; type JUint = u64; // }}} // 1.4 Date and UTCDate data Types {{{ /// Where date is given a type, it meanse a string in date-time format [RFC3339]. /// TODO: To ensure a normalized form, the time-setfrac MUST always be ommitted if zero, and any /// letters in the string (eg. "T" and "Z") must be uppercase. #[derive(Deserialize, Serialize, Clone, Debug)] pub struct JDate(DateTime); impl JDate { /// Convert from a JUTCDate type to system time pub fn from_utcdate(date: JDate) -> Self { // FIXME: unwrap Self(date.0.to_zoned(jiff::tz::TimeZone::system()).unwrap().datetime()) } /// Parse from a string pub fn from_string(str: &str) -> Result { Ok(Self(str.parse()?)) } /// Get the inner date for reading purposes pub fn get_date(&self) -> &DateTime { &self.0 } /// Convert from jiff datetime pub fn from_datetime(datetime: DateTime) -> Self { Self(datetime) } } /// Where UTCDate is given as a type, it means a Date where the time-offset component MUST be Z (i.e., it must be in UTC time). For example, 2014-10-30T06:12:00Z #[derive(Deserialize, Serialize, Clone, Debug)] pub struct JUTCDate(DateTime); impl JUTCDate { /// Convert from a JDate type pub fn from_date(date: JDate) -> Self { // FIXME: unwrap Self(date.0.to_zoned(jiff::tz::TimeZone::UTC).unwrap().datetime()) } /// Parse from a string pub fn from_string(str: &str) -> Result { Ok(Self( str.parse::()? .to_zoned(jiff::tz::TimeZone::UTC) .datetime(), )) } /// Get the inner date for reading purposes pub fn get_date(&self) -> &DateTime { &self.0 } /// Convert from jiff datetime pub fn from_datetime(datetime: DateTime) -> Self { // FIXME: unwrap Self( datetime .to_zoned(jiff::tz::TimeZone::UTC) .unwrap() .datetime(), ) } } //}}} // 2: JMAP Session Resource {{{ // Capabilities {{{ pub trait Capability { // Get urn of the capability fn get_urn(&self) -> &'static str; // Convert to any for downcasting, immutable fn as_any(&self) -> &dyn Any; /// Convert to any for downcasting, mutable fn as_any_mut(&mut self) -> &mut dyn Any; /// Convert to any for downcasting, consumes object fn to_any(self) -> Box; /// Given a capability and accountCapabilities Object get a capability fn account_cap(&self, account_id: JMAPId, obj: &JObject) -> Box; /// Return the account if this capabilities has been given info for account fn account(&self) -> Option; } /// A function that parses a JObject into a Capability Trait object. /// This capability will be able to implement functionality to the JMAPSession object. pub type CapabilityParser = Box Box>; /// Collection of CapabilityParser pub struct CapabilityParsers(pub BTreeMap); impl Default for CapabilityParsers { fn default() -> Self { let mut c = Self::new(); c.insert(URN_JMAP_CORE.to_string(), Box::new(JMAPCoreCapability::from_object)); c } } impl CapabilityParsers { /// Create a collection of parsers pub fn new() -> Self { Self(BTreeMap::new()) } /// Insert a parser pub fn insert(&mut self, urn: String, parser: CapabilityParser) { self.0.insert(urn, parser); } /// Use the parsers to create a list of capabilities supported by the server in the session. pub fn server_capabilities(&self, session: &ISession) -> ServerCapabilities { let mut out = BTreeMap::new(); for (urn, cap) in session.capabilities.iter() { if let Some(parser) = self.0.get(urn) { out.insert(urn.clone(), parser(cap.clone())); }; } ServerCapabilities(out) } } /// The backing datatype in the CapabilityCollection trait. pub type CapabilityMap = BTreeMap>; /// Trait for list of capabilities, or any URN -> Capabilty collection trait CapabilityCollection { fn inner(&self) -> &CapabilityMap; fn inner_mut(&mut self) -> &mut CapabilityMap; fn get_capability(&self, urn: &str) -> Option<&T> { self.inner().get(urn)?.as_any().downcast_ref::() } fn get_capability_mut(&mut self, urn: &str) -> Option<&mut T> { self.inner_mut() .get_mut(urn)? .as_any_mut() .downcast_mut::() } } /// Collection pub struct ServerCapabilities(pub CapabilityMap); impl CapabilityCollection for ServerCapabilities { fn inner(&self) -> &CapabilityMap { &self.0 } fn inner_mut(&mut self) -> &mut CapabilityMap { &mut self.0 } } impl std::fmt::Debug for ServerCapabilities { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("ServerCapabilities") .field(&self.0.keys().collect::>()) .finish() } } impl ServerCapabilities { /// Evaluate AccountCapabilities using this list of capabilities and the intermediate session pub fn account_capabilities( &self, client: &JMAPId, session: &ISession, ) -> Option { session.accounts.get(client).map(|acc| { let mut out = BTreeMap::new(); let empty = JObj::new(); for (urn, scap) in self.0.iter() { let ccap = acc.accountCapabilities.get(urn).unwrap_or(&empty); out.insert(urn.clone(), scap.account_cap(client.clone(), ccap)); } AccountCapabilities(out) }) } } /// List of capabilities, specific to an account. pub struct AccountCapabilities(pub CapabilityMap); impl std::fmt::Debug for AccountCapabilities { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("AccountCapabilities") .field(&self.0.keys().collect::>()) .finish() } } impl CapabilityCollection for AccountCapabilities { fn inner(&self) -> &CapabilityMap { &self.0 } fn inner_mut(&mut self) -> &mut CapabilityMap { &mut self.0 } } // JMAP Core capability {{{ /// JMAP Core URN pub const URN_JMAP_CORE: &'static str = "urn:ietf:params:jmap:core"; /// JMAP Core capability as outlined in RFC 8620 #[allow(non_snake_case)] #[derive(Serialize, Deserialize, Clone)] pub struct JMAPCoreCapability { pub maxSizeUpload: JUint, pub maxConcurrentUpload: JUint, pub maxSizeRequest: JUint, pub maxConcurrentRequests: JUint, pub maxCallsInRequest: JUint, pub maxObjectsInGet: JUint, pub maxObjectsInSet: JUint, pub collationAlgorithms: JArr, #[serde(skip)] accountId: Option, } impl Capability for JMAPCoreCapability { fn get_urn(&self) -> &'static str { URN_JMAP_CORE } fn as_any(&self) -> &dyn Any { self } fn as_any_mut(&mut self) -> &mut dyn Any { self } fn to_any(self) -> Box { Box::new(self) } fn account_cap(&self, account_id: JMAPId, _: &JObject) -> Box { let mut out: Box = Box::new(self.clone()); out.accountId = Some(account_id); out } fn account(&self) -> Option { self.accountId.clone() } } impl JMAPCoreCapability { fn from_object(obj: JObject) -> Box { // TODO: Unwrap let out: Self = serde_json::from_value(JUnknown::Object(obj).into()).unwrap(); Box::new(out) } } // }}} // }}} /// Intermediate Serde-Powered Account Struct #[allow(non_snake_case)] #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ISessionAccount { pub name: String, pub isPersonal: bool, pub isReadOnly: bool, pub accountCapabilities: JObj, } /// Intermediate Serde-Powered Session Struct #[allow(non_snake_case)] #[derive(Serialize, Deserialize, Debug)] pub struct ISession { pub capabilities: JObj, pub accounts: JObj, pub primaryAccounts: JObj, pub username: String, pub apiUrl: String, pub downloadUrl: String, pub eventSourceUrl: String, pub state: String, } //}}} // 2.a Library specifics for the Session Resource {{{ /// Fully evaluated jmap account #[derive(Debug)] pub struct JMAPAccount { pub base: ISessionAccount, pub capabilities: AccountCapabilities, } /// Evaluated session (the accounts are evaluated lazily) #[derive(Debug)] pub struct JMAPSession { base: ISession, capabilities: ServerCapabilities, accounts: BTreeMap>, } impl JMAPSession { /// Get session from the json pub fn from_json(json: &str, parsers: CapabilityParsers) -> Result { let session: ISession = serde_json::from_str(json)?; Ok(Self::from_intermediate(session, parsers)) } /// Get session from the ISession object pub fn from_intermediate(session: ISession, parsers: CapabilityParsers) -> Self { let mut accounts = BTreeMap::new(); for acc in session.accounts.keys() { accounts.insert(acc.clone(), None); } Self { capabilities: parsers.server_capabilities(&session), base: session, accounts, } } /// Show all the available accounts pub fn list_accounts(&self) -> Keys<'_, JMAPId, Option> { self.accounts.keys() } /// Get all the json-based account intermediate structs in an iterator pub fn intermediate_accounts(&self) -> Iter<'_, JMAPId, ISessionAccount> { self.base.accounts.iter() } /// Get a singular json-base account intermediate pub fn get_intermediate_account(&self, account_id: &JMAPId) -> Option<&ISessionAccount> { self.base.accounts.get(account_id) } /// Get the fully evaluated account if it was initialized pub fn get_account(&self, account_id: &JMAPId) -> Option<&JMAPAccount> { if let Some(acc) = self.accounts.get(account_id) { acc.as_ref() } else { None } } /// Get the fully evaluated account if it was initialized, mutable pub fn get_account_mut(&mut self, account_id: &JMAPId) -> Option<&mut JMAPAccount> { if let Some(acc) = self.accounts.get_mut(account_id) { acc.as_mut() } else { None } } /// Evaluate account fn eval_account(&mut self, account_id: &JMAPId) -> Option { let evaluated_account = self.accounts.get(account_id).is_some_and(|x| x.is_some()); if evaluated_account { return Some(false); } if let Some(capabilities) = self .capabilities .account_capabilities(account_id, &self.base) { let base = self .get_intermediate_account(account_id) .expect("Already checked") .clone(); let acc = self .accounts .get_mut(account_id) .expect("Accounts and intermediateAccounts have the same keys"); *acc = Some(JMAPAccount { base, capabilities }); Some(true) } else { None } } /// get_account that evaluates the account if necessary pub fn eval_get_account(&mut self, account_id: &JMAPId) -> Option<&JMAPAccount> { if self.eval_account(account_id).is_some() { Some( self.get_account(account_id) .expect("We should have made this account just now"), ) } else { None } } /// get_account_mut that evaluates the account if necessary pub fn eval_get_account_mut(&mut self, account_id: &JMAPId) -> Option<&mut JMAPAccount> { if self.eval_account(account_id).is_some() { Some( self.get_account_mut(account_id) .expect("We should have made this account just now"), ) } else { None } } } // }}} // Appendix A: tests {{{ #[cfg(test)] mod test { use crate::{CapabilityCollection, JMAPCoreCapability, JMAPId, URN_JMAP_CORE}; use std::assert_matches; #[test] fn intermediate_parse_test() { let in1 = r#" { "capabilities": { "urn:ietf:params:jmap:core": { "maxSizeUpload": 50000000, "maxConcurrentUpload": 8, "maxSizeRequest": 10000000, "maxConcurrentRequests": 8, "maxCallsInRequest": 32, "maxObjectsInGet": 256, "maxObjectsInSet": 128, "collationAlgorithms": [ "i;ascii-numeric", "i;ascii-casemap", "i;unicode-casemap" ] }, "urn:ietf:params:jmap:mail": {}, "urn:ietf:params:jmap:contacts": {}, "https://example.com/apis/foobar": { "maxFoosFinangled": 42 } }, "accounts": { "A13824": { "name": "john@example.com", "isPersonal": true, "isReadOnly": false, "accountCapabilities": { "urn:ietf:params:jmap:mail": { "maxMailboxesPerEmail": null, "maxMailboxDepth": 10 }, "urn:ietf:params:jmap:contacts": { } } }, "A97813": { "name": "jane@example.com", "isPersonal": false, "isReadOnly": true, "accountCapabilities": { "urn:ietf:params:jmap:mail": { "maxMailboxesPerEmail": 1, "maxMailboxDepth": 10 } } } }, "primaryAccounts": { "urn:ietf:params:jmap:mail": "A13824", "urn:ietf:params:jmap:contacts": "A13824" }, "username": "john@example.com", "apiUrl": "https://jmap.example.com/api/", "downloadUrl": "https://jmap.example.com/download/{accountId}/{blobId}/{name}?accept={type}", "uploadUrl": "https://jmap.example.com/upload/{accountId}/", "eventSourceUrl": "https://jmap.example.com/eventsource/?types={types}&closeafter={closeafter}&ping={ping}", "state": "75128aab4b1b" } "#; let intermediate: Result = serde_json::from_str(in1); assert_matches!(intermediate, Ok(_)); println!("{:?}", intermediate); } #[test] fn basic_init() { let in1 = r#" { "capabilities": { "urn:ietf:params:jmap:core": { "maxSizeUpload": 50000000, "maxConcurrentUpload": 8, "maxSizeRequest": 10000000, "maxConcurrentRequests": 8, "maxCallsInRequest": 32, "maxObjectsInGet": 256, "maxObjectsInSet": 128, "collationAlgorithms": [ "i;ascii-numeric", "i;ascii-casemap", "i;unicode-casemap" ] }, "urn:ietf:params:jmap:mail": {}, "urn:ietf:params:jmap:contacts": {}, "https://example.com/apis/foobar": { "maxFoosFinangled": 42 } }, "accounts": { "A13824": { "name": "john@example.com", "isPersonal": true, "isReadOnly": false, "accountCapabilities": { "urn:ietf:params:jmap:mail": { "maxMailboxesPerEmail": null, "maxMailboxDepth": 10 }, "urn:ietf:params:jmap:contacts": { } } } }, "primaryAccounts": { "urn:ietf:params:jmap:mail": "A13824", "urn:ietf:params:jmap:contacts": "A13824" }, "username": "john@example.com", "apiUrl": "https://jmap.example.com/api/", "downloadUrl": "https://jmap.example.com/download/{accountId}/{blobId}/{name}?accept={type}", "uploadUrl": "https://jmap.example.com/upload/{accountId}/", "eventSourceUrl": "https://jmap.example.com/eventsource/?types={types}&closeafter={closeafter}&ping={ping}", "state": "75128aab4b1b" } "#; let mut parsers = crate::CapabilityParsers::new(); parsers.insert( URN_JMAP_CORE.to_string(), Box::new(JMAPCoreCapability::from_object), ); let mut session = match crate::JMAPSession::from_json(in1, parsers) { Ok(i) => i, Err(e) => unreachable!("Error in parsing json {:?}", e), }; print!("{:?}", session); let list: Vec = session.list_accounts().map(Clone::clone).collect(); assert_eq!(list, vec![JMAPId("A13824".to_string())]); assert!( session .capabilities .get_capability::(URN_JMAP_CORE) .is_some() ); let account = session.get_account(&crate::JMAPId("A13824".to_string())); assert_matches!(account, None); let account_opt = session.eval_get_account(&crate::JMAPId("A13824".to_string())); assert_matches!(account_opt, Some(_)); let account_opt = session.eval_get_account_mut(&crate::JMAPId("A13824".to_string())); let account = account_opt.unwrap(); assert_eq!( account .capabilities .0 .keys() .map(Clone::clone) .collect::>(), vec![URN_JMAP_CORE] ); let cap: &mut JMAPCoreCapability = account .capabilities .get_capability_mut(URN_JMAP_CORE) .unwrap(); assert_eq!(cap.accountId, Some(JMAPId("A13824".to_string()))); } } // }}} // vim: set foldmethod=marker