summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJakub Stachurski <jakub@wilkuu.nl>2026-10-05 18:28:39 +0200
committerJakub Stachurski <jakub@wilkuu.nl>2026-10-05 18:28:39 +0200
commit91a465bea6a9d199348492e37820006532cff0b2 (patch)
treea557395bc663ee8343355e8d2c034ea5ebcf5dab /src
parentfcfd9ac33e36010a9da5d7f6133fa1d8c014ad51 (diff)
request: better tests and request building
Diffstat (limited to 'src')
-rw-r--r--src/capabilities.rs10
-rw-r--r--src/request.rs167
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<Self> {
+ Ok(Self { obj: obj.clone() })
}
- fn into_obj(self) -> JObject {
- self.obj
+ fn into_obj(self) -> JMAPResult<JObject> {
+ 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<dyn JMAPInvocationArgDyn> {
- Box::new(Self::from_obj(obj))
+ fn from_obj(obj: &JObject) -> JMAPResult<Self>;
+ fn from_obj_dyn(obj: &JObject) -> JMAPResult<Box<dyn JMAPInvocationArgDyn>> {
+ let arg = Self::from_obj(obj)?;
+ Ok(Box::new(arg))
}
- fn into_obj(self) -> JObject;
+ fn into_obj(self) -> JMAPResult<JObject>;
fn from_dyn(d: Box<dyn JMAPInvocationArgDyn>) -> JMAPResult<Box<Self>> {
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<Self>) -> JMAPResult<JObject>;
// Convert to any for downcasting, immutable
fn as_any(&self) -> &dyn Any;
@@ -146,8 +147,8 @@ impl<T: JMAPInvocationArg> 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<Self>) -> JMAPResult<JObject> {
+ T::into_obj(*self)
}
fn as_any(&self) -> &dyn Any {
@@ -184,6 +185,22 @@ impl<T: JMAPInvocationArg> Invocation<T> {
method_call_id: d.method_call_id,
})
}
+ pub fn to_raw(self) -> JMAPResult<InvocationRaw> {
+ 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<InvocationRaw>{
+ 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<InvocationRaw>,
#[serde(rename = "createIds")]
+ #[serde(skip_serializing_if = "Option::is_none")]
create_ids: Option<JObj<JMAPId, JMAPId>>,
}
@@ -218,7 +249,7 @@ pub struct JMAPRequest {
pub create_ids: Option<JObj<JMAPId, JMAPId>>,
}
-pub type MethodArgsParser = Box<fn(&JObject) -> Box<dyn JMAPInvocationArgDyn>>;
+pub type MethodArgsParser = Box<fn(&JObject) -> JMAPResult<Box<dyn JMAPInvocationArgDyn>>>;
pub struct MethodRegistry(pub BTreeMap<JMAPMethodName, MethodArgsParser>);
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<JMAPRequestRaw> {
+ 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<T: JMAPInvocationArg>(&mut self, invocation: Invocation<T>) -> 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(), &reg, true);
- assert_matches!(req_err, Err(JMAPError::UnknownCapability(_)));
let req_ok = JMAPRequest::from_raw(raw, &reg, 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(), &reg, 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(), &reg, 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);
+ }
}