diff --git a/melib/src/jmap/errors.rs b/melib/src/jmap/errors.rs new file mode 100644 index 00000000..03b7c730 --- /dev/null +++ b/melib/src/jmap/errors.rs @@ -0,0 +1,224 @@ +// +// meli +// +// Copyright 2024 Emmanouil Pitsidianakis +// +// This file is part of meli. +// +// meli is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// meli is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with meli. If not, see . +// +// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later + +use indexmap::IndexMap; + +pub type Result = Result; + +//{ +// "type": "urn:ietf:params:jmap:error:unknownCapability", +// "status": 400, +// "detail": "The Request object used capability +// 'https://example.com/apis/foobar', which is not supported +// by this server." +// } +//} + +//A problem details object can have the following members: +// +// +/// +// +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ProblemDetails { + /// ```text + /// o "type" (string) - A URI reference [RFC3986] that identifies the + /// problem type. This specification encourages that, when + /// dereferenced, it provide human-readable documentation for the + /// problem type (e.g., using HTML [W3C.REC-html5-20141028]). When + /// this member is not present, its value is assumed to be + /// "about:blank". + /// ``` + #[serde(default, skip_serializing_if = "ProblemDetailsType::is_blank")] + pub r#type: ProblemDetailsType, + #[serde(default, skip_serializing_if = "Option::is_none")] + /// ```text + /// o "status" (number) - The HTTP status code ([RFC7231], Section 6) + /// generated by the origin server for this occurrence of the problem. + /// ``` + pub status: Option, + /// ```text + /// o "title" (string) - A short, human-readable summary of the problem + /// type. It SHOULD NOT change from occurrence to occurrence of the + /// problem, except for purposes of localization (e.g., using + /// proactive content negotiation; see [RFC7231], Section 3.4). + /// ``` + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option, + /// ```text + /// o "detail" (string) - A human-readable explanation specific to this + /// occurrence of the problem. + /// ``` + #[serde(default, skip_serializing_if = "Option::is_none")] + pub detail: Option, + /// ```text + /// o "instance" (string) - A URI reference that identifies the specific + /// occurrence of the problem. It may or may not yield further + /// information if dereferenced. + /// ``` + #[serde(default, skip_serializing_if = "Option::is_none")] + pub instance: Option, + /// Specific errors may have more fields and hence metadata. + #[serde(flatten, skip_serializing_if = "IndexMap::is_empty")] + pub extra_properties: IndexMap, +} + +impl ProblemDetails { + pub const CONTENT_TYPE: &'static str = "application/problem+json"; +} + +//3.6.1. Request-Level Errors +// +// When an HTTP error response is returned to the client, the server +// SHOULD return a JSON "problem details" object as the response body, +// as per [RFC7807]. +// +// The following problem types are defined: +// +// o "urn:ietf:params:jmap:error:unknownCapability" +// The client included a capability in the "using" property of the +// request that the server does not support. +// +// o "urn:ietf:params:jmap:error:notJSON" +// The content type of the request was not "application/json" or the +// request did not parse as I-JSON. +// +// o "urn:ietf:params:jmap:error:notRequest" +// The request parsed as JSON but did not match the type signature of +// the Request object. +// +// o "urn:ietf:params:jmap:error:limit" +// The request was not processed as it would have exceeded one of the +// request limits defined on the capability object, such as +// maxSizeRequest, maxCallsInRequest, or maxConcurrentRequests. A +// "limit" property MUST also be present on the "problem details" +// object, containing the name of the limit being applied. +// +//3.6.1.1. Example +// +// { +// "type": "urn:ietf:params:jmap:error:unknownCapability", +// "status": 400, +// "detail": "The Request object used capability +// 'https://example.com/apis/foobar', which is not supported +// by this server." +// } +// +// +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)] +#[repr(u8)] +pub enum ProblemDetailsType { + #[default] + Blank = 0, + UnknownCapability = 1, + NotJson = 2, + NotRequest = 3, + Limit = 4, +} + +impl ProblemDetailsType { + pub const URIS: [&'static str; 5] = [ + "about:blank", + "urn:ietf:params:jmap:error:unknownCapability", + "urn:ietf:params:jmap:error:notJSON", + "urn:ietf:params:jmap:error:notRequest", + "urn:ietf:params:jmap:error:limit", + ]; + + pub const fn is_blank(&self) -> bool { + matches!(self, Self::Blank) + } +} + +impl ::serde::ser::Serialize for ProblemDetailsType { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: ::serde::ser::Serializer, + { + serializer.serialize_str(Self::URIS[*self as u8 as usize]) + } +} + +impl<'de> ::serde::de::Deserialize<'de> for ProblemDetailsType { + fn deserialize(deserializer: D) -> std::result::Result + where + D: ::serde::de::Deserializer<'de>, + { + use serde::de::{Error, Unexpected, Visitor}; + use ProblemDetailsType as S; + + struct _Visitor; + + impl<'de> Visitor<'de> for _Visitor { + type Value = S; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str( + "a string representing a valid Problem Type URI as per RFC7807 - Problem \ + Details for HTTP APIs", + ) + } + + fn visit_str(self, s: &str) -> std::result::Result + where + E: Error, + { + Ok(match s { + _ if s == S::URIS[S::Blank as u8 as usize] => S::Blank, + _ if s == S::URIS[S::UnknownCapability as u8 as usize] => S::UnknownCapability, + _ if s == S::URIS[S::NotJson as u8 as usize] => S::NotJson, + _ if s == S::URIS[S::NotRequest as u8 as usize] => S::NotRequest, + _ if s == S::URIS[S::Limit as u8 as usize] => S::Limit, + _ => { + return Err(Error::invalid_value( + Unexpected::Str(s), + &format!("Expected one of: {}", S::URIS.as_slice().join(",")).as_str(), + )) + } + }) + } + } + + deserializer.deserialize_str(_Visitor) + } +} + +impl ::std::fmt::Display for ProblemDetailsType { + fn fmt(&self, fmt: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(fmt, "{}", Self::URIS[*self as u8 as usize]) + } +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum MethodError { + RequestTooLarge, + InvalidArguments, + InvalidResultReference, +} + +#[derive(Clone, Copy, Debug)] +pub enum JmapError { + Method { inner: MethodError }, + Request { inner: ProblemDetails }, + Other, +} diff --git a/melib/src/jmap/methods.rs b/melib/src/jmap/methods.rs index 6ec49049..da02d63f 100644 --- a/melib/src/jmap/methods.rs +++ b/melib/src/jmap/methods.rs @@ -35,7 +35,9 @@ use crate::{ jmap::{ argument::Argument, comparator::Comparator, - deserialize_from_str, filters, + deserialize_from_str, + error::JmapError, + filters, objects::{Account, BlobObject, Id, Object, PatchObject, State}, protocol::Method, session::Session, @@ -195,7 +197,7 @@ pub struct GetResponse { } impl std::convert::TryFrom<&RawValue> for GetResponse { - type Error = crate::error::Error; + type Error = JmapError; fn try_from(t: &RawValue) -> Result { let res: (String, Self, String) = deserialize_from_str(t.get())?; @@ -211,14 +213,6 @@ impl GetResponse { _impl!(get_mut not_found_mut, not_found: Vec>); } -#[derive(Clone, Copy, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -enum JmapError { - RequestTooLarge, - InvalidArguments, - InvalidResultReference, -} - #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Query, OBJ> diff --git a/melib/src/jmap/mod.rs b/melib/src/jmap/mod.rs index 8b54fc24..fb7ef12d 100644 --- a/melib/src/jmap/mod.rs +++ b/melib/src/jmap/mod.rs @@ -48,7 +48,6 @@ use crate::{ backends::*, conf::AccountSettings, email::*, - error::{Error, ErrorKind, Result}, utils::futures::{sleep, timeout}, Collection, }; @@ -104,6 +103,7 @@ pub mod argument; pub mod capabilities; pub mod comparator; pub mod email; +pub mod errors; pub mod filters; pub mod identity; pub mod mailbox; @@ -112,6 +112,7 @@ pub mod thread; use argument::Argument; use capabilities::JmapCoreCapability; +use errors::{JmapError, Result}; use filters::Filter; #[cfg(test)] diff --git a/melib/src/jmap/tests.rs b/melib/src/jmap/tests.rs index 1ad31b1a..ad95aee0 100644 --- a/melib/src/jmap/tests.rs +++ b/melib/src/jmap/tests.rs @@ -696,3 +696,42 @@ fn test_jmap_session_serde() { .unwrap(), ); } + +#[test] +fn test_jmap_problem_details_error() { + use serde_json::json; + + use crate::jmap::errors::{ProblemDetails, ProblemDetailsType}; + + const RFC_EXAMPLE: &str = r###"{ +"type": "urn:ietf:params:jmap:error:unknownCapability", +"status": 400, +"detail": "The Request object used capability 'https://example.com/apis/foobar', which is not supported by this server." +}"###; + + let problem_details_obj = ProblemDetails { + r#type: ProblemDetailsType::UnknownCapability, + status: Some(400), + detail: Some( + "The Request object used capability 'https://example.com/apis/foobar', which is not \ + supported by this server." + .into(), + ), + title: None, + instance: None, + extra_properties: Default::default(), + }; + + assert_eq!( + &serde_json::from_str::(RFC_EXAMPLE).unwrap(), + &problem_details_obj + ); + assert_eq!( + json!(problem_details_obj), + serde_json::from_str::(RFC_EXAMPLE).unwrap(), + ); + assert_eq!( + &serde_json::from_str::(&json!(problem_details_obj).to_string()).unwrap(), + &problem_details_obj, + ); +}