summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore9
-rw-r--r--Cargo.lock256
-rw-r--r--Cargo.toml3
-rwxr-xr-xcoverage.sh22
-rw-r--r--flake.nix3
-rw-r--r--src/lib.rs677
-rw-r--r--src/main.rs3
7 files changed, 969 insertions, 4 deletions
diff --git a/.gitignore b/.gitignore
index ea8c4bf..8cb21fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 5aca40a..ef9e2ea 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
diff --git a/flake.nix b/flake.nix
index 10dd5d7..ac25326 100644
--- a/flake.nix
+++ b/flake.nix
@@ -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!");
-}