summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakub Stachurski <jakub@wilkuu.nl>2026-08-22 00:59:22 +0200
committerJakub Stachurski <jakub@wilkuu.nl>2026-08-22 00:59:22 +0200
commit030c489d2bc34f267cadacdec8cd13249672ccb5 (patch)
treeff7b67753acf1fbc313655334784728809079f40
parent1047933fcb9e5b80401eae5e7e188dfd1fe32a91 (diff)
Make Capability into CapabilityDyn and Add non-dyn Capability trait
-rw-r--r--src/capabilities.rs109
-rw-r--r--src/lib.rs172
-rw-r--r--src/session.rs113
3 files changed, 169 insertions, 225 deletions
diff --git a/src/capabilities.rs b/src/capabilities.rs
index 63a526c..63aa6e8 100644
--- a/src/capabilities.rs
+++ b/src/capabilities.rs
@@ -1,6 +1,8 @@
use std::any::Any;
use std::collections::BTreeMap;
+use serde::{Deserialize, Serialize};
+
use crate::session::ISession;
use crate::*;
@@ -9,7 +11,23 @@ use crate::*;
pub const URN_JMAP_CORE: &str = "urn:ietf:params:jmap:core";
// Capabilities {{{
-pub trait Capability {
+
+pub trait Capability: 'static + Sized {
+ const URN: &str;
+
+ fn from_object(obj: &JObject) -> Self;
+ fn account_cap(&self, account_id: JMAPId, obj: &JObject) -> Self;
+ fn account(&self) -> Option<JMAPId>;
+ fn from_object_dyn(obj: &JObject) -> Box<dyn CapabilityDyn> {
+ Box::new(Self::from_object(obj))
+ }
+
+ fn is_account_capability(&self) -> bool {
+ self.account().is_some()
+ }
+}
+
+pub trait CapabilityDyn {
// Get urn of the capability
fn get_urn(&self) -> &'static str;
@@ -23,38 +41,61 @@ pub trait Capability {
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>;
+ fn account_cap_dyn(&self, account_id: JMAPId, obj: &JObject) -> Box<dyn CapabilityDyn>;
/// Return the account if this capabilities has been given info for account
- fn account(&self) -> Option<JMAPId>;
+ fn account_dyn(&self) -> Option<JMAPId>;
+}
+
+impl<T: Capability> CapabilityDyn for T {
+ fn get_urn(&self) -> &'static str {
+ T::URN
+ }
+ 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_dyn(&self, account_id: JMAPId, obj: &JObject) -> Box<dyn CapabilityDyn> {
+ Box::new(self.account_cap(account_id, obj))
+ }
+
+ fn account_dyn(&self) -> Option<JMAPId> {
+ self.account()
+ }
}
/// 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>>;
+pub type CapabilityParser = Box<dyn Fn(&JObject) -> Box<dyn CapabilityDyn>>;
/// Collection of CapabilityParser
-pub struct CapabilityParsers(pub BTreeMap<String, CapabilityParser>);
+pub struct CapabilityRegistry(pub BTreeMap<String, CapabilityParser>);
-impl Default for CapabilityParsers {
+impl Default for CapabilityRegistry {
fn default() -> Self {
let mut c = Self::new();
- c.insert(
- URN_JMAP_CORE.to_string(),
- Box::new(JMAPCoreCapability::from_object),
- );
+ c.add_capability::<JMAPCoreCapability>();
c
}
}
-impl CapabilityParsers {
+impl CapabilityRegistry {
/// 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);
+ pub fn add_capability<T: Capability>(&mut self) {
+ self.0
+ .insert(T::URN.to_string(), Box::new(T::from_object_dyn));
}
/// Use the parsers to create a list of capabilities supported by the server in the session.
@@ -62,7 +103,7 @@ impl CapabilityParsers {
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()));
+ out.insert(urn.clone(), parser(cap));
};
}
ServerCapabilities(out)
@@ -70,20 +111,20 @@ impl CapabilityParsers {
}
/// The backing datatype in the CapabilityCollection trait.
-pub type CapabilityMap = BTreeMap<String, Box<dyn Capability>>;
+pub type CapabilityMap = BTreeMap<String, Box<dyn CapabilityDyn>>;
/// 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<T: Capability>(&self) -> Option<&T> {
+ self.inner().get(T::URN)?.as_any().downcast_ref::<T>()
}
- fn get_capability_mut<T: 'static + Capability>(&mut self, urn: &str) -> Option<&mut T> {
+ fn get_capability_mut<T: Capability>(&mut self) -> Option<&mut T> {
self.inner_mut()
- .get_mut(urn)?
+ .get_mut(T::URN)?
.as_any_mut()
.downcast_mut::<T>()
}
@@ -122,7 +163,7 @@ impl ServerCapabilities {
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));
+ out.insert(urn.clone(), scap.account_cap_dyn(client.clone(), ccap));
}
AccountCapabilities(out)
})
@@ -167,23 +208,16 @@ pub struct JMAPCoreCapability {
}
impl Capability for JMAPCoreCapability {
- fn get_urn(&self) -> &'static str {
- URN_JMAP_CORE
- }
- fn as_any(&self) -> &dyn Any {
- self
- }
+ const URN: &str = URN_JMAP_CORE;
- fn as_any_mut(&mut self) -> &mut dyn Any {
- self
- }
-
- fn to_any(self) -> Box<dyn Any> {
- Box::new(self)
+ fn from_object(obj: &JObject) -> Self {
+ // TODO: Unwrap
+ let out: Self = serde_json::from_value(JUnknown::Object(obj.clone()).into()).unwrap();
+ out
}
- fn account_cap(&self, account_id: JMAPId, _: &JObject) -> Box<dyn Capability> {
- let mut out: Box<JMAPCoreCapability> = Box::new(self.clone());
+ fn account_cap(&self, account_id: JMAPId, _: &JObject) -> Self {
+ let mut out = self.clone();
out.accountId = Some(account_id);
out
}
@@ -193,11 +227,4 @@ impl Capability for JMAPCoreCapability {
}
}
-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 5bda018..b877f9d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,171 +1,7 @@
-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(),
- )
- }
-}
-//}}}
+// 1.1 Notational Conventions
+// types
+pub mod types;
+pub use types::*;
pub mod capabilities;
/// 2 JMAP Session resource
diff --git a/src/session.rs b/src/session.rs
index 82a3563..b9e508e 100644
--- a/src/session.rs
+++ b/src/session.rs
@@ -3,7 +3,7 @@ use std::any::Any;
use std::collections::BTreeMap;
use std::collections::btree_map::{Iter, Keys};
-use crate::capabilities::{AccountCapabilities, CapabilityParsers, ServerCapabilities};
+use crate::capabilities::{AccountCapabilities, CapabilityRegistry, ServerCapabilities};
use crate::*;
// 2: JMAP Session Resource {{{
@@ -50,13 +50,13 @@ pub struct JMAPSession {
impl JMAPSession {
/// Get session from the json
- pub fn from_json(json: &str, parsers: CapabilityParsers) -> Result<Self, serde_json::Error> {
+ pub fn from_json(json: &str, parsers: CapabilityRegistry) -> 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 {
+ pub fn from_intermediate(session: ISession, parsers: CapabilityRegistry) -> Self {
let mut accounts = BTreeMap::new();
for acc in session.accounts.keys() {
accounts.insert(acc.clone(), None);
@@ -155,12 +155,63 @@ impl JMAPSession {
// Appendix A: tests {{{
#[cfg(test)]
mod test {
- use crate::JMAPId;
+ use serde::{Deserialize, Serialize};
+ use serde_json::Value;
+
use crate::capabilities::{
- Capability, CapabilityCollection, JMAPCoreCapability, URN_JMAP_CORE,
+ AccountCapabilities, Capability, CapabilityCollection, CapabilityDyn, JMAPCoreCapability,
};
+ use crate::{JArr, JDate, JMAPId, JNumber, JObj, JUTCDate, JUnknown, from_junknown};
use std::assert_matches;
+ enum DeserializeTestCapability {
+ Session(DTC_Session),
+ Account(DTC_Session, DTC_Account),
+ }
+
+ #[derive(Serialize, Deserialize, Clone)]
+ struct DTC_Session {
+ num: JNumber,
+ numArr: JArr<JNumber>,
+ bool: bool,
+ str: String,
+ boolObj: JObj<JMAPId, bool>,
+ date: JDate,
+ }
+
+ #[derive(Serialize, Deserialize, Clone)]
+ struct DTC_Account {
+ #[serde(skip)]
+ account_id: Option<JMAPId>,
+ utc_date: JUTCDate,
+ }
+
+ impl Capability for DeserializeTestCapability {
+ const URN: &str = "urn:xyz:wilkuu:deser_test";
+
+ fn from_object(obj: &crate::JObject) -> Self {
+ Self::Session(from_junknown(obj.clone()).unwrap())
+ }
+
+ fn account_cap(&self, account_id: JMAPId, obj: &crate::JObject) -> Self {
+ let session_cap = (match self {
+ Self::Session(s) => s,
+ Self::Account(s, _) => s,
+ })
+ .clone();
+ let mut acc: DTC_Account = from_junknown(obj.clone()).unwrap();
+ acc.account_id = Some(account_id);
+ Self::Account(session_cap, acc)
+ }
+
+ fn account(&self) -> Option<JMAPId> {
+ match self {
+ Self::Session(_) => None,
+ Self::Account(_, a) => Some(a.account_id.clone().expect("This should always be Some, considering that we create it with the account_id in account_cap")),
+ }
+ }
+ }
+
#[test]
fn intermediate_parse_test() {
let in1 = r#"
@@ -254,6 +305,14 @@ mod test {
"urn:ietf:params:jmap:contacts": {},
"https://example.com/apis/foobar": {
"maxFoosFinangled": 42
+ },
+ "urn:xyz:wilkuu:deser_test": {
+ "num": 12.0,
+ "numArr": [1,2.0,3.0],
+ "bool": true,
+ "str": "string",
+ "boolObj": { "a": true, "b": false },
+ "date": "2026-08-10T10:00:00+02:00"
}
},
"accounts": {
@@ -267,13 +326,17 @@ mod test {
"maxMailboxDepth": 10
},
"urn:ietf:params:jmap:contacts": {
- }
+ },
+ "urn:xyz:wilkuu:deser_test": {
+ "utc_date": "2026-08-10T12:00:00Z"
+ }
}
}
},
"primaryAccounts": {
"urn:ietf:params:jmap:mail": "A13824",
- "urn:ietf:params:jmap:contacts": "A13824"
+ "urn:ietf:params:jmap:contacts": "A13824",
+ "urn:xyz:wilkuu_deser_test": "A13824"
},
"username": "john@example.com",
"apiUrl": "https://jmap.example.com/api/",
@@ -284,7 +347,9 @@ mod test {
}
"#;
- let parsers = crate::session::CapabilityParsers::default();
+ let mut parsers = crate::session::CapabilityRegistry::default();
+ parsers.add_capability::<DeserializeTestCapability>();
+
let mut session = match crate::session::JMAPSession::from_json(in1, parsers) {
Ok(i) => i,
Err(e) => unreachable!("Error in parsing json {:?}", e),
@@ -296,13 +361,13 @@ mod test {
assert!(
session
.capabilities
- .get_capability::<JMAPCoreCapability>(URN_JMAP_CORE)
+ .get_capability::<JMAPCoreCapability>()
.is_some()
);
let account = session.get_account(&crate::JMAPId("A13824".to_string()));
- assert_matches!(account, None);
+ assert!(account.is_none());
let account_opt = session.eval_get_account(&crate::JMAPId("A13824".to_string()));
- assert_matches!(account_opt, Some(_));
+ assert!(account_opt.is_some());
let account_opt = session.eval_get_account_mut(&crate::JMAPId("A13824".to_string()));
let account = account_opt.unwrap();
assert_eq!(
@@ -312,13 +377,29 @@ mod test {
.keys()
.map(Clone::clone)
.collect::<Vec<String>>(),
- vec![URN_JMAP_CORE]
+ vec![JMAPCoreCapability::URN, DeserializeTestCapability::URN]
);
- let cap: &mut JMAPCoreCapability = account
- .capabilities
- .get_capability_mut(URN_JMAP_CORE)
- .unwrap();
+ let cap: &mut JMAPCoreCapability = account.capabilities.get_capability_mut().unwrap();
assert_eq!(cap.account(), Some(JMAPId("A13824".to_string())));
+
+ let deser_cap: &mut DeserializeTestCapability =
+ account.capabilities.get_capability_mut().unwrap();
+ assert_eq!(deser_cap.account(), Some(JMAPId("A13824".to_string())));
+ match deser_cap {
+ DeserializeTestCapability::Session(_) => unreachable!("This should be an account cap"),
+ DeserializeTestCapability::Account(s, _a) => {
+ assert_eq!(s.num, 12.0);
+ assert_eq!(s.numArr, vec![1.0, 2.0, 3.0]);
+ assert!(s.bool);
+ assert_eq!(s.str, "string");
+ assert_eq!(s.boolObj.get(&JMAPId("a".to_string())), Some(true).as_ref());
+ assert_eq!(
+ s.boolObj.get(&JMAPId("b".to_string())),
+ Some(false).as_ref()
+ );
+ // TODO: Test dates
+ }
+ }
}
}
// }}}