diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/capabilities.rs | 203 | ||||
| -rw-r--r-- | src/lib.rs | 529 | ||||
| -rw-r--r-- | src/session.rs | 326 |
3 files changed, 541 insertions, 517 deletions
diff --git a/src/capabilities.rs b/src/capabilities.rs new file mode 100644 index 0000000..63a526c --- /dev/null +++ b/src/capabilities.rs @@ -0,0 +1,203 @@ +use std::any::Any; +use std::collections::BTreeMap; + +use crate::session::ISession; +use crate::*; + +// JMAP Core capability {{{ +/// JMAP Core URN +pub const URN_JMAP_CORE: &str = "urn:ietf:params:jmap:core"; + +// 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<dyn Any>; + + /// Given a capability and accountCapabilities Object get a capability + fn account_cap(&self, account_id: JMAPId, obj: &JObject) -> Box<dyn Capability>; + + /// Return the account if this capabilities has been given info for account + fn account(&self) -> Option<JMAPId>; +} + +/// 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<dyn Fn(JObject) -> Box<dyn Capability>>; +/// Collection of CapabilityParser +pub struct CapabilityParsers(pub BTreeMap<String, CapabilityParser>); + +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<String, Box<dyn Capability>>; + +/// Trait for list of capabilities, or any URN -> Capabilty collection +pub trait CapabilityCollection { + fn inner(&self) -> &CapabilityMap; + fn inner_mut(&mut self) -> &mut CapabilityMap; + + fn get_capability<T: 'static + Capability>(&self, urn: &str) -> Option<&T> { + self.inner().get(urn)?.as_any().downcast_ref::<T>() + } + + fn get_capability_mut<T: 'static + Capability>(&mut self, urn: &str) -> Option<&mut T> { + self.inner_mut() + .get_mut(urn)? + .as_any_mut() + .downcast_mut::<T>() + } +} + +/// 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::<Vec<&String>>()) + .finish() + } +} + +impl ServerCapabilities { + /// Evaluate AccountCapabilities using this list of capabilities and the intermediate session + pub fn account_capabilities( + &self, + client: &JMAPId, + session: &ISession, + ) -> Option<AccountCapabilities> { + 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::<Vec<&String>>()) + .finish() + } +} +impl CapabilityCollection for AccountCapabilities { + fn inner(&self) -> &CapabilityMap { + &self.0 + } + + fn inner_mut(&mut self) -> &mut CapabilityMap { + &mut self.0 + } +} + +// }}} +/// 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<String>, + #[serde(skip)] + accountId: Option<JMAPId>, +} + +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<dyn Any> { + Box::new(self) + } + + fn account_cap(&self, account_id: JMAPId, _: &JObject) -> Box<dyn Capability> { + let mut out: Box<JMAPCoreCapability> = Box::new(self.clone()); + out.accountId = Some(account_id); + out + } + + fn account(&self) -> Option<JMAPId> { + self.accountId.clone() + } +} + +impl JMAPCoreCapability { + fn from_object(obj: JObject) -> Box<dyn Capability> { + // TODO: Unwrap + let out: Self = serde_json::from_value(JUnknown::Object(obj).into()).unwrap(); + Box::new(out) + } +} +// }}} @@ -28,7 +28,7 @@ pub enum JUnknown { } /// Implement from for serde_json::Value so that we can use serde to covert unknown to actual types -/// :3 +/// :3 impl From<JUnknown> for Value { fn from(value: JUnknown) -> Self { match value { @@ -84,8 +84,8 @@ where // 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], +/// 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)] @@ -108,7 +108,12 @@ 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()) + Self( + date.0 + .to_zoned(jiff::tz::TimeZone::system()) + .unwrap() + .datetime(), + ) } /// Parse from a string pub fn from_string(str: &str) -> Result<Self, jiff::Error> { @@ -161,517 +166,7 @@ impl JUTCDate { } } //}}} -// 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<dyn Any>; - - /// Given a capability and accountCapabilities Object get a capability - fn account_cap(&self, account_id: JMAPId, obj: &JObject) -> Box<dyn Capability>; - - /// Return the account if this capabilities has been given info for account - fn account(&self) -> Option<JMAPId>; -} - - -/// 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<dyn Fn(JObject) -> Box<dyn Capability>>; -/// Collection of CapabilityParser -pub struct CapabilityParsers(pub BTreeMap<String, CapabilityParser>); - -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<String, Box<dyn Capability>>; - -/// 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<T: 'static + Capability>(&self, urn: &str) -> Option<&T> { - self.inner().get(urn)?.as_any().downcast_ref::<T>() - } - - fn get_capability_mut<T: 'static + Capability>(&mut self, urn: &str) -> Option<&mut T> { - self.inner_mut() - .get_mut(urn)? - .as_any_mut() - .downcast_mut::<T>() - } -} - -/// 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::<Vec<&String>>()) - .finish() - } -} - -impl ServerCapabilities { - /// Evaluate AccountCapabilities using this list of capabilities and the intermediate session - pub fn account_capabilities( - &self, - client: &JMAPId, - session: &ISession, - ) -> Option<AccountCapabilities> { - 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::<Vec<&String>>()) - .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<String>, - #[serde(skip)] - accountId: Option<JMAPId>, -} - -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<dyn Any> { - Box::new(self) - } - - fn account_cap(&self, account_id: JMAPId, _: &JObject) -> Box<dyn Capability> { - let mut out: Box<JMAPCoreCapability> = Box::new(self.clone()); - out.accountId = Some(account_id); - out - } - - fn account(&self) -> Option<JMAPId> { - self.accountId.clone() - } -} - -impl JMAPCoreCapability { - fn from_object(obj: JObject) -> Box<dyn Capability> { - // 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<String, JObject>, -} - -/// Intermediate Serde-Powered Session Struct -#[allow(non_snake_case)] -#[derive(Serialize, Deserialize, Debug)] -pub struct ISession { - pub capabilities: JObj<String, JObject>, - pub accounts: JObj<JMAPId, ISessionAccount>, - pub primaryAccounts: JObj<String, JMAPId>, - 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<JMAPId, Option<JMAPAccount>>, -} - -impl JMAPSession { - /// Get session from the json - pub fn from_json(json: &str, parsers: CapabilityParsers) -> Result<Self, serde_json::Error> { - 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<JMAPAccount>> { - 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<bool> { - 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<crate::ISession, serde_json::Error> = 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<JMAPId> = session.list_accounts().map(Clone::clone).collect(); - assert_eq!(list, vec![JMAPId("A13824".to_string())]); - assert!( - session - .capabilities - .get_capability::<JMAPCoreCapability>(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<String>>(), - 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 +pub mod capabilities; +/// 2 JMAP Session resource +pub mod session; diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000..82a3563 --- /dev/null +++ b/src/session.rs @@ -0,0 +1,326 @@ +use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::collections::BTreeMap; +use std::collections::btree_map::{Iter, Keys}; + +use crate::capabilities::{AccountCapabilities, CapabilityParsers, ServerCapabilities}; +use crate::*; + +// 2: JMAP Session Resource {{{ + +/// 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<String, JObject>, +} + +/// Intermediate Serde-Powered Session Struct +#[allow(non_snake_case)] +#[derive(Serialize, Deserialize, Debug)] +pub struct ISession { + pub capabilities: JObj<String, JObject>, + pub accounts: JObj<JMAPId, ISessionAccount>, + pub primaryAccounts: JObj<String, JMAPId>, + 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<JMAPId, Option<JMAPAccount>>, +} + +impl JMAPSession { + /// Get session from the json + pub fn from_json(json: &str, parsers: CapabilityParsers) -> Result<Self, serde_json::Error> { + 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<JMAPAccount>> { + 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<bool> { + 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::JMAPId; + use crate::capabilities::{ + Capability, CapabilityCollection, JMAPCoreCapability, 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<crate::session::ISession, serde_json::Error> = + 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 parsers = crate::session::CapabilityParsers::default(); + let mut session = match crate::session::JMAPSession::from_json(in1, parsers) { + Ok(i) => i, + Err(e) => unreachable!("Error in parsing json {:?}", e), + }; + print!("{:?}", session); + + let list: Vec<JMAPId> = session.list_accounts().map(Clone::clone).collect(); + assert_eq!(list, vec![JMAPId("A13824".to_string())]); + assert!( + session + .capabilities + .get_capability::<JMAPCoreCapability>(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<String>>(), + vec![URN_JMAP_CORE] + ); + let cap: &mut JMAPCoreCapability = account + .capabilities + .get_capability_mut(URN_JMAP_CORE) + .unwrap(); + assert_eq!(cap.account(), Some(JMAPId("A13824".to_string()))); + } +} +// }}} + +// vim: set foldmethod=marker |
