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, } /// 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::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 = 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 = 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.account(), Some(JMAPId("A13824".to_string()))); } } // }}} // vim: set foldmethod=marker