use std::any::Any; use std::collections::BTreeMap; use std::error::Error; use std::fmt::{Debug, Display}; use std::str::FromStr; use crate::capabilities::{CapabilityRegistry, URN_JMAP_CORE}; use crate::types::*; use serde::{Deserialize, Serialize}; // TODO: Move to an error module pub type JMAPResult = Result; #[derive(Debug)] pub enum JMAPError { ParseError(String), DowncastError(String), UnknownCapability(String), UnknownMethod(String), InvalidObject(String), } impl Display for JMAPError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) //TODO: Make this less bad } } impl Error for JMAPError {} #[derive(Clone, PartialEq, PartialOrd, Ord, Eq, Debug)] pub struct JMAPMethodName { pub resource: String, pub method: String, } impl JMAPMethodName { pub fn new(resource: String, method: String) -> Self { assert_ne!(resource.len(), 0); assert_ne!(method.len(), 0); Self { resource, method } } } impl FromStr for JMAPMethodName { type Err = JMAPError; fn from_str(s: &str) -> Result { let mut spl = s.split("/"); let resource = spl .next() .expect("Always at least one string expected from `split`") .to_string(); let method = (match spl.next() { Some(m) => { if m.is_empty() { Err(JMAPError::ParseError( "Missing method name after '/'".to_string(), )) } else { Ok(m) } } None => Err(JMAPError::ParseError("Missing '/' delimiter".to_string())), })? .to_string(); if let Some(t) = spl.next() { Err(JMAPError::ParseError(format!( "Additional '/' found => '/{}'", t ))) } else { Ok(Self { resource, method }) } } } impl Display for JMAPMethodName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}/{}", self.resource, self.method) } } #[derive(Serialize, Deserialize, Clone)] pub struct InvocationRaw(String, JObject, String); pub trait JMAPInvocationArg: 'static + Sized { const CAP_URN: &str; const METHOD_HINT: &'static str; fn from_obj(obj: &JObject) -> JMAPResult; fn from_obj_dyn(obj: &JObject) -> JMAPResult> { let arg = Self::from_obj(obj)?; Ok(Box::new(arg)) } fn into_obj(self) -> JMAPResult; fn from_dyn(d: Box) -> JMAPResult> { let dyn_hint = d.get_method_hint(); if dyn_hint != Self::METHOD_HINT { return Err(JMAPError::DowncastError(format!( "Incompatible arguments: {} vs. {} ", Self::METHOD_HINT, dyn_hint ))); } let rf = Box::leak(d); if let Some(anyref) = rf.as_any_mut().downcast_mut() { // Safe because the reference is leaked from another box unsafe { Ok(Box::from_raw(anyref)) } } else { // Safe because the reference is leaked from another box unsafe { // Free the memory let b = Box::from_raw(rf); drop(b) } Err(JMAPError::DowncastError( "T is not the correct type despite having same method hint".to_string(), )) } } } pub trait JMAPInvocationArgDyn: 'static { fn get_urn(&self) -> &'static str; fn get_method_hint(&self) -> &'static str; fn into_obj(self: Box) -> JMAPResult; // 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; } impl JMAPInvocationArgDyn for T { fn get_urn(&self) -> &'static str { T::CAP_URN } fn get_method_hint(&self) -> &'static str { T::METHOD_HINT } fn into_obj(self: Box) -> JMAPResult { T::into_obj(*self) } fn as_any(&self) -> &dyn Any { self } fn as_any_mut(&mut self) -> &mut dyn Any { self } fn to_any(self) -> Box { Box::new(self) } } pub struct Invocation { pub method: JMAPMethodName, pub arguments: T, pub method_call_id: String, } impl Invocation { pub fn to_dyn(self) -> InvocationDyn { InvocationDyn { method: self.method, arguments: Box::new(self.arguments), method_call_id: self.method_call_id, } } pub fn from_dyn(d: InvocationDyn) -> JMAPResult { Ok(Self { method: d.method, arguments: *T::from_dyn(d.arguments)?, method_call_id: d.method_call_id, }) } pub fn to_raw(self) -> JMAPResult { let args = self.arguments.into_obj()?; Ok(InvocationRaw ( self.method.to_string(), args, self.method_call_id )) } pub fn new(arguments: T, method_call_id: String) -> Self { Self { method: T::METHOD_HINT.parse().expect("METHOD_HINT MUST Contain a valid method name"), arguments, method_call_id, } } } pub struct InvocationDyn { method: JMAPMethodName, arguments: Box, method_call_id: String, } impl InvocationDyn { pub fn to_raw(self) -> JMAPResult{ let args = self.arguments; let args_obj = args.into_obj()?; Ok(InvocationRaw ( self.method.to_string(), args_obj, self.method_call_id )) } } impl Debug for InvocationDyn { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "InvocationDyn({}, *, {})", self.method, self.method_call_id ) } } #[derive(Serialize, Deserialize, Clone)] pub struct JMAPRequestRaw { using: JArr, #[serde(rename = "methodCalls")] method_calls: JArr, #[serde(rename = "createIds")] #[serde(skip_serializing_if = "Option::is_none")] create_ids: Option>, } #[derive(Debug)] pub struct JMAPRequest { pub using: JArr, pub method_calls: JArr>, pub create_ids: Option>, } pub type MethodArgsParser = Box JMAPResult>>; pub struct MethodRegistry(pub BTreeMap); impl Default for MethodRegistry { fn default() -> Self { Self::new() } } impl MethodRegistry { /// Create a collection of parsers pub fn new() -> Self { Self(BTreeMap::new()) } /// Insert a parser pub fn add_method(&mut self) { self.0.insert( T::METHOD_HINT .parse() .expect("METHOD_HINT needs to be a valid Method Name"), Box::new(T::from_obj_dyn), ); } } impl Default for JMAPRequest { fn default() -> Self { Self { using: vec![URN_JMAP_CORE.to_string()], method_calls: vec![], create_ids: None, } } } impl JMAPRequest { pub fn from_raw( raw: JMAPRequestRaw, registry: &CapabilityRegistry, fail_on_unknown_capability: bool, ) -> JMAPResult { let using = raw.using; let create_ids = raw.create_ids; let mut method_registries = Vec::with_capacity(using.len()); let mut encountered_urns = Vec::with_capacity(using.len()); let mut has_core = false; for cap_urn in using.iter() { if cap_urn == URN_JMAP_CORE { has_core = true; } if encountered_urns.contains(&cap_urn) { return Err(JMAPError::InvalidObject(format!( "Dupplicate capability {}", cap_urn ))); } encountered_urns.push(cap_urn); if let Some(method_reg) = registry.0.get(cap_urn) { method_registries.push(&method_reg.methods); } else if fail_on_unknown_capability { return Err(JMAPError::UnknownCapability(cap_urn.clone())); } } if !has_core { return Err(JMAPError::InvalidObject( "Missing core capability".to_string(), )); } let method_calls: Vec> = raw .method_calls .into_iter() .map(|method_call| { let mut method_parser_opt: Option<&MethodArgsParser> = None; match method_call.0.parse() { Ok(method_name) => { for reg in method_registries.iter() { if let Some(parser) = reg.0.get(&method_name) { method_parser_opt = Some(parser); break; } } if let Some(method_parser) = method_parser_opt { let method_args = method_parser(&method_call.1)?; Ok(InvocationDyn { method: method_name, arguments: method_args, method_call_id: method_call.2, }) } else { Err(JMAPError::UnknownMethod(method_name.to_string())) } } Err(err) => Err(err), } }) .collect(); Ok(Self { using, method_calls, create_ids, }) } pub fn to_raw(self) -> JMAPResult { let mut method_calls = Vec::with_capacity(self.method_calls.len()); for mcr in self.method_calls { let mc = mcr?; method_calls.push(mc.to_raw()?) } Ok(JMAPRequestRaw { using: self.using, method_calls, create_ids: self.create_ids }) } pub fn add_invocation(&mut self, invocation: Invocation) -> JMAPResult<()>{ match self.using.iter().find(|x| x.as_str() == T::CAP_URN) { Some(_) => {}, None => { self.using.push(T::CAP_URN.to_string()); } // TODO: Turn into set } self.method_calls.push(Ok(invocation.to_dyn())); Ok(()) } } #[cfg(test)] mod test { use jiff::fmt::rfc2822::parse; use serde_json::json; use super::{JMAPRequest, JMAPRequestRaw}; use crate::capabilities::{CapabilityRegistry, CoreEchoArgs, URN_JMAP_CORE}; use crate::request::{Invocation, InvocationDyn, JMAPError, JMAPResult}; use crate::{JMAPId, JObject, JUnknown}; use std::assert_matches; use std::str::FromStr; #[test] fn test_jmap_request_parsing() { let req = r#"{ "using": ["urn:ietf:params:jmap:core", "something_unknown"], "methodCalls": [ [ "Core/Echo", { "hello": "Hello world!" }, "a" ], [ "Unknown/Get", { "something": "else" }, "b" ] ], "createIds": { "i1": "id1" } }"#; let reg = CapabilityRegistry::default(); let raw: JMAPRequestRaw = serde_json::from_str(req).unwrap(); let req_ok = JMAPRequest::from_raw(raw, ®, false); assert_matches!( req_ok, Ok(JMAPRequest { using: _, method_calls: _, create_ids: _ }) ); let mut req_real = req_ok.expect("We tested this before"); assert_eq!( req_real .create_ids .expect("We specified this in the test") .get(&("i1".parse().unwrap())) .expect("i1 Should be included") .clone(), JMAPId::from_str("id1").unwrap() ); assert_eq!(req_real.using, vec![URN_JMAP_CORE, "something_unknown"]); let unknown = req_real.method_calls.get(1).unwrap(); assert_matches!(unknown, Err(JMAPError::UnknownMethod(_))); let echo = req_real.method_calls.remove(0).unwrap(); let invocation: Invocation = Invocation::from_dyn(echo).unwrap(); assert_eq!( invocation.arguments.obj.get("hello"), Some(&JUnknown::String("Hello world!".to_string())) ); } #[test] fn unknown_capability() { let req = r#"{ "using": ["urn:ietf:params:jmap:core", "something_unknown"], "methodCalls": [ [ "Core/Echo", { "hello": "Hello world!" }, "a" ], [ "Unknown/Get", { "something": "else" }, "b" ] ], "createIds": { "i1": "id1" } }"#; let reg = CapabilityRegistry::default(); let raw: JMAPRequestRaw = serde_json::from_str(req).unwrap(); let req_err = JMAPRequest::from_raw(raw.clone(), ®, true); assert_matches!(req_err, JMAPResult::Err(JMAPError::UnknownCapability(_))) } #[test] fn dupplicate_capability() { let req = r#"{ "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:core"], "methodCalls": [ [ "Core/Echo", { "hello": "Hello world!" }, "a" ] ], "createIds": { "i1": "id1" } }"#; let reg = CapabilityRegistry::default(); let raw: JMAPRequestRaw = serde_json::from_str(req).unwrap(); let req_err = JMAPRequest::from_raw(raw.clone(), ®, true); assert_matches!(req_err, JMAPResult::Err(JMAPError::InvalidObject(_))) } #[test] fn builder() { let mut req = JMAPRequest::default(); let content: JObject = serde_json::from_value(json!( { "hello": "Hello world!" } )).unwrap(); let echo = CoreEchoArgs {obj: content} ; let inv = Invocation::new(echo, "a".to_string()); req.add_invocation(inv).unwrap(); let rraw = req.to_raw().unwrap(); let exp = json!({ "using": ["urn:ietf:params:jmap:core"], "methodCalls": [ [ "Core/Echo", { "hello": "Hello world!" }, "a" ] ] }); assert_eq!(serde_json::to_value(&rraw).unwrap(), exp); } }