summaryrefslogtreecommitdiff
path: root/src/request.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/request.rs')
-rw-r--r--src/request.rs390
1 files changed, 390 insertions, 0 deletions
diff --git a/src/request.rs b/src/request.rs
new file mode 100644
index 0000000..3a31d4a
--- /dev/null
+++ b/src/request.rs
@@ -0,0 +1,390 @@
+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<T> = Result<T, JMAPError>;
+
+#[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<Self, Self::Err> {
+ 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) -> Self;
+ fn from_obj_dyn(obj: &JObject) -> Box<dyn JMAPInvocationArgDyn> {
+ Box::new(Self::from_obj(obj))
+ }
+ fn into_obj(self) -> JObject;
+
+ fn from_dyn(d: Box<dyn JMAPInvocationArgDyn>) -> JMAPResult<Box<Self>> {
+ 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) -> JObject;
+
+ // 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<dyn Any>;
+}
+
+impl<T: JMAPInvocationArg> 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) -> JObject {
+ self.into_obj()
+ }
+
+ 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)
+ }
+}
+
+pub struct Invocation<T: JMAPInvocationArg> {
+ pub method: JMAPMethodName,
+ pub arguments: T,
+ pub method_call_id: String,
+}
+
+impl<T: JMAPInvocationArg> Invocation<T> {
+ 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<Self> {
+ Ok(Self {
+ method: d.method,
+ arguments: *T::from_dyn(d.arguments)?,
+ method_call_id: d.method_call_id,
+ })
+ }
+}
+
+pub struct InvocationDyn {
+ method: JMAPMethodName,
+ arguments: Box<dyn JMAPInvocationArgDyn>,
+ method_call_id: String,
+}
+
+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<String>,
+ #[serde(rename = "methodCalls")]
+ method_calls: JArr<InvocationRaw>,
+ #[serde(rename = "createIds")]
+ create_ids: Option<JObj<JMAPId, JMAPId>>,
+}
+
+#[derive(Debug)]
+pub struct JMAPRequest {
+ pub using: JArr<String>,
+ pub method_calls: JArr<JMAPResult<InvocationDyn>>,
+ pub create_ids: Option<JObj<JMAPId, JMAPId>>,
+}
+
+pub type MethodArgsParser = Box<fn(&JObject) -> Box<dyn JMAPInvocationArgDyn>>;
+pub struct MethodRegistry(pub BTreeMap<JMAPMethodName, MethodArgsParser>);
+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<T: JMAPInvocationArg>(&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 JMAPRequest {
+ pub fn from_raw(
+ raw: JMAPRequestRaw,
+ registry: &CapabilityRegistry,
+ fail_on_unknown_capability: bool,
+ ) -> JMAPResult<Self> {
+ 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<JMAPResult<InvocationDyn>> = 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,
+ })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use jiff::fmt::rfc2822::parse;
+
+ use super::{JMAPRequest, JMAPRequestRaw};
+ use crate::capabilities::{CapabilityRegistry, CoreEchoArgs, URN_JMAP_CORE};
+ use crate::request::{Invocation, InvocationDyn, JMAPError};
+ use crate::{JMAPId, 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_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,
+ 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<CoreEchoArgs> = Invocation::from_dyn(echo).unwrap();
+
+ assert_eq!(
+ invocation.arguments.obj.get("hello"),
+ Some(&JUnknown::String("Hello world!".to_string()))
+ );
+ }
+}