Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
jmap-again
Manos Pitsidianakis 3 months ago
parent 7eed944abc
commit 21c622a9a4
No known key found for this signature in database
GPG Key ID: 7729C7707F7E09D0

@ -0,0 +1,224 @@
//
// meli
//
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// 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 <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
use indexmap::IndexMap;
pub type Result<T> = Result<T, JmapError>;
//{
// "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<u16>,
/// ```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<String>,
/// ```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<String>,
/// ```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<String>,
/// Specific errors may have more fields and hence metadata.
#[serde(flatten, skip_serializing_if = "IndexMap::is_empty")]
pub extra_properties: IndexMap<String, serde_json::Value>,
}
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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
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<D>(deserializer: D) -> std::result::Result<Self, D::Error>
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<E>(self, s: &str) -> std::result::Result<Self::Value, E>
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,
}

@ -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<OBJ: Object> {
}
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for GetResponse<OBJ> {
type Error = crate::error::Error;
type Error = JmapError;
fn try_from(t: &RawValue) -> Result<Self> {
let res: (String, Self, String) = deserialize_from_str(t.get())?;
@ -211,14 +213,6 @@ impl<OBJ: Object> GetResponse<OBJ> {
_impl!(get_mut not_found_mut, not_found: Vec<Id<OBJ>>);
}
#[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<F: filters::FilterTrait<OBJ>, OBJ>

@ -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)]

@ -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::<ProblemDetails>(RFC_EXAMPLE).unwrap(),
&problem_details_obj
);
assert_eq!(
json!(problem_details_obj),
serde_json::from_str::<serde_json::Value>(RFC_EXAMPLE).unwrap(),
);
assert_eq!(
&serde_json::from_str::<ProblemDetails>(&json!(problem_details_obj).to_string()).unwrap(),
&problem_details_obj,
);
}

Loading…
Cancel
Save