summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/capabilities.rs203
-rw-r--r--src/lib.rs529
-rw-r--r--src/session.rs326
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)
+ }
+}
+// }}}
diff --git a/src/lib.rs b/src/lib.rs
index f8f2ce8..5bda018 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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