diff options
| author | Jakub Stachurski <jakub@wilkuu.nl> | 2026-08-22 00:59:22 +0200 |
|---|---|---|
| committer | Jakub Stachurski <jakub@wilkuu.nl> | 2026-08-22 00:59:22 +0200 |
| commit | 030c489d2bc34f267cadacdec8cd13249672ccb5 (patch) | |
| tree | ff7b67753acf1fbc313655334784728809079f40 /src | |
| parent | 1047933fcb9e5b80401eae5e7e188dfd1fe32a91 (diff) | |
Make Capability into CapabilityDyn and Add non-dyn Capability trait
Diffstat (limited to '')
| -rw-r--r-- | src/capabilities.rs | 109 | ||||
| -rw-r--r-- | src/lib.rs | 172 | ||||
| -rw-r--r-- | src/session.rs | 113 |
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) - } -} // }}} @@ -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 + } + } } } // }}} |
