diff options
| author | Jakub Stachurski <jakub@wilkuu.nl> | 2026-08-14 22:14:08 +0200 |
|---|---|---|
| committer | Jakub Stachurski <jakub@wilkuu.nl> | 2026-08-14 22:14:08 +0200 |
| commit | af0b5d6ce4e5b9901afe50656f0016e1db72f2c5 (patch) | |
| tree | f27bf0a825fdcd35e1a653fdad407f0dc039ca6b | |
| parent | 4d1cb750d5b2f26f5257cacc74a70476349d1c9e (diff) | |
Initial commitmaster
| -rw-r--r-- | .gitignore | 9 | ||||
| -rw-r--r-- | Cargo.lock | 256 | ||||
| -rw-r--r-- | Cargo.toml | 3 | ||||
| -rwxr-xr-x | coverage.sh | 22 | ||||
| -rw-r--r-- | flake.nix | 3 | ||||
| -rw-r--r-- | src/lib.rs | 677 | ||||
| -rw-r--r-- | src/main.rs | 3 |
7 files changed, 969 insertions, 4 deletions
@@ -1 +1,10 @@ /target +.envrc +.direnv + + +# Added by cargo +# +# already existing elements were commented out + +#/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..079b9ec --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,256 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "libjmap" +version = "0.1.0" +dependencies = [ + "jiff", + "serde", + "serde_json", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" @@ -4,3 +4,6 @@ version = "0.1.0" edition = "2024" [dependencies] +jiff = { version = "0.2.35", features = ["serde"] } +serde = { version = "1.0.229", features = ["derive", "rc"] } +serde_json = "1.0.151" diff --git a/coverage.sh b/coverage.sh new file mode 100755 index 0000000..7532015 --- /dev/null +++ b/coverage.sh @@ -0,0 +1,22 @@ +#/usr/bin/env bash +LLVM_PROFILE_FILE=default_%m_%p.profraw + +_output=`RUSTFLAGS="$RUSTFLAGS -C instrument-coverage" cargo test --message-format=json` +_cmd="$1"; shift +llvm-profdata merge -sparse default_*.profraw -o default.profdata + +llvm-cov $_cmd \ + $( \ + for file in \ + $( \ + echo $_output \ + | jq -r "select(.profile.test == true) | .filenames[]" \ + | grep -v dSYM - \ + ); \ + do \ + printf "%s %s " -object $file; \ + done \ + ) --instr-profile=default.profdata --use-color --ignore-filename-regex='/.cargo/registry' $@ # and/or other options + +rm default.profdata +rm default_*.profraw @@ -33,7 +33,7 @@ rustToolchain = fenix.packages.${system}.fromToolchainFile { file = ./rust-toolchain.toml; - sha256 = "sha256-LAQdG4kB6olIXlVFbq610fgUl9hCTtgnYgX7kZQfE+Q="; + sha256 = "sha256-A1abGIbOtcBSdrUMhDGrER3pRM1hQP4fp9gh3Y4PKc8="; }; craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; @@ -99,6 +99,7 @@ packages = with pkgs; [ # Put your packages for dev here + libllvm ]; env = { diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f8f2ce8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,677 @@ +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<String, JUnknown>), + Array(Vec<JUnknown>), + 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<JUnknown> 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<A, B> +where + A: Serialize, + B: Serialize, + A: DeserializeOwned, + B: DeserializeOwned, += BTreeMap<A, B>; + +pub type JObject = JObj<String, JUnknown>; +/// A[] is an array of type A +pub type JArr<A> +where + A: Serialize, + A: DeserializeOwned, += Vec<A>; + +//}}} +// 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<Self, jiff::Error> { + 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<Self, jiff::Error> { + Ok(Self( + str.parse::<jiff::Timestamp>()? + .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<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 diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index e7a11a9..0000000 --- a/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -} |
