From 91a465bea6a9d199348492e37820006532cff0b2 Mon Sep 17 00:00:00 2001 From: Jakub Stachurski Date: Mon, 5 Oct 2026 18:28:39 +0200 Subject: request: better tests and request building --- src/capabilities.rs | 10 ++-- src/request.rs | 167 ++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 159 insertions(+), 18 deletions(-) diff --git a/src/capabilities.rs b/src/capabilities.rs index 1a4aa35..2d3dbe7 100644 --- a/src/capabilities.rs +++ b/src/capabilities.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use lazy_static; use serde::{Deserialize, Serialize}; -use crate::request::{JMAPInvocationArg, MethodRegistry}; +use crate::request::{JMAPInvocationArg, JMAPResult, MethodRegistry}; use crate::session::ISession; use crate::*; @@ -231,12 +231,12 @@ impl JMAPInvocationArg for CoreEchoArgs { const METHOD_HINT: &str = "Core/Echo"; - fn from_obj(obj: &JObject) -> Self { - Self { obj: obj.clone() } + fn from_obj(obj: &JObject) -> JMAPResult { + Ok(Self { obj: obj.clone() }) } - fn into_obj(self) -> JObject { - self.obj + fn into_obj(self) -> JMAPResult { + Ok(self.obj) } } diff --git a/src/request.rs b/src/request.rs index 3a31d4a..301cd2e 100644 --- a/src/request.rs +++ b/src/request.rs @@ -90,11 +90,12 @@ 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) -> Self; - fn from_obj_dyn(obj: &JObject) -> Box { - Box::new(Self::from_obj(obj)) + 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) -> JObject; + fn into_obj(self) -> JMAPResult; fn from_dyn(d: Box) -> JMAPResult> { let dyn_hint = d.get_method_hint(); @@ -127,7 +128,7 @@ pub trait JMAPInvocationArg: 'static + Sized { pub trait JMAPInvocationArgDyn: 'static { fn get_urn(&self) -> &'static str; fn get_method_hint(&self) -> &'static str; - fn into_obj(self) -> JObject; + fn into_obj(self: Box) -> JMAPResult; // Convert to any for downcasting, immutable fn as_any(&self) -> &dyn Any; @@ -146,8 +147,8 @@ impl JMAPInvocationArgDyn for T { fn get_method_hint(&self) -> &'static str { T::METHOD_HINT } - fn into_obj(self) -> JObject { - self.into_obj() + fn into_obj(self: Box) -> JMAPResult { + T::into_obj(*self) } fn as_any(&self) -> &dyn Any { @@ -184,6 +185,22 @@ impl Invocation { 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 { @@ -192,6 +209,19 @@ pub struct InvocationDyn { 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!( @@ -208,6 +238,7 @@ pub struct JMAPRequestRaw { #[serde(rename = "methodCalls")] method_calls: JArr, #[serde(rename = "createIds")] + #[serde(skip_serializing_if = "Option::is_none")] create_ids: Option>, } @@ -218,7 +249,7 @@ pub struct JMAPRequest { pub create_ids: Option>, } -pub type MethodArgsParser = Box Box>; +pub type MethodArgsParser = Box JMAPResult>>; pub struct MethodRegistry(pub BTreeMap); impl Default for MethodRegistry { fn default() -> Self { @@ -243,6 +274,17 @@ impl MethodRegistry { } } +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, @@ -292,7 +334,7 @@ impl JMAPRequest { } } if let Some(method_parser) = method_parser_opt { - let method_args = method_parser(&method_call.1); + let method_args = method_parser(&method_call.1)?; Ok(InvocationDyn { method: method_name, arguments: method_args, @@ -313,16 +355,40 @@ impl JMAPRequest { 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}; - use crate::{JMAPId, JUnknown}; + use crate::request::{Invocation, InvocationDyn, JMAPError, JMAPResult}; + use crate::{JMAPId, JObject, JUnknown}; use std::assert_matches; use std::str::FromStr; @@ -351,8 +417,6 @@ mod test { 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, Err(JMAPError::UnknownCapability(_))); let req_ok = JMAPRequest::from_raw(raw, ®, false); assert_matches!( req_ok, @@ -387,4 +451,81 @@ mod test { 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); + } } -- cgit v1.3.1