use core::{error::Error, fmt};
use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use url::Url;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JmapSession {
pub username: String,
pub accounts: BTreeMap<String, JmapAccountInfo>,
pub primary_accounts: BTreeMap<String, String>,
pub capabilities: BTreeMap<String, Value>,
pub api_url: Url,
pub download_url: String,
pub upload_url: String,
pub event_source_url: String,
pub state: String,
}
impl JmapSession {
pub fn primary_account_id_for(&self, capability: &str) -> String {
self.primary_accounts
.get(capability)
.cloned()
.unwrap_or_default()
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JmapAccountInfo {
pub name: String,
pub is_personal: bool,
pub is_read_only: bool,
pub account_capabilities: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum JmapMethodError {
ServerFail {
description: Option<String>,
},
ServerPartialFail,
ServerUnavailable {
description: Option<String>,
},
UnknownCapability {
description: Option<String>,
},
NotJson {
description: Option<String>,
},
NotRequest {
description: Option<String>,
},
Limit {
description: Option<String>,
limit: String,
},
InvalidArguments {
description: Option<String>,
},
RequestTooLarge,
NotFound,
InvalidPatch {
description: Option<String>,
},
WillDestroy {
description: Option<String>,
},
InvalidProperties {
description: Option<String>,
#[serde(default)]
properties: Vec<String>,
},
Singleton,
UnknownMethod {
description: Option<String>,
},
CannotCalculateChanges {
description: Option<String>,
},
#[serde(other)]
Unknown,
}
impl fmt::Display for JmapMethodError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ServerFail { description } => {
write!(f, "JMAP serverFail")?;
if let Some(d) = description {
write!(f, ": {d}")?;
}
Ok(())
}
Self::ServerPartialFail => write!(f, "JMAP serverPartialFail"),
Self::ServerUnavailable { description } => {
write!(f, "JMAP serverUnavailable")?;
if let Some(d) = description {
write!(f, ": {d}")?;
}
Ok(())
}
Self::UnknownCapability { description } => {
write!(f, "JMAP unknownCapability")?;
if let Some(d) = description {
write!(f, ": {d}")?;
}
Ok(())
}
Self::NotJson { description } => {
write!(f, "JMAP notJson")?;
if let Some(d) = description {
write!(f, ": {d}")?;
}
Ok(())
}
Self::NotRequest { description } => {
write!(f, "JMAP notRequest")?;
if let Some(d) = description {
write!(f, ": {d}")?;
}
Ok(())
}
Self::Limit { description, limit } => {
write!(f, "JMAP limit ({limit})")?;
if let Some(d) = description {
write!(f, ": {d}")?;
}
Ok(())
}
Self::InvalidArguments { description } => {
write!(f, "JMAP invalidArguments")?;
if let Some(d) = description {
write!(f, ": {d}")?;
}
Ok(())
}
Self::RequestTooLarge => write!(f, "JMAP requestTooLarge"),
Self::NotFound => write!(f, "JMAP notFound"),
Self::InvalidPatch { description } => {
write!(f, "JMAP invalidPatch")?;
if let Some(d) = description {
write!(f, ": {d}")?;
}
Ok(())
}
Self::WillDestroy { description } => {
write!(f, "JMAP willDestroy")?;
if let Some(d) = description {
write!(f, ": {d}")?;
}
Ok(())
}
Self::InvalidProperties {
description,
properties,
} => {
write!(f, "JMAP invalidProperties")?;
if !properties.is_empty() {
write!(f, " [{}]", properties.join(", "))?;
}
if let Some(d) = description {
write!(f, ": {d}")?;
}
Ok(())
}
Self::Singleton => write!(f, "JMAP singleton"),
Self::UnknownMethod { description } => {
write!(f, "JMAP unknownMethod")?;
if let Some(d) = description {
write!(f, ": {d}")?;
}
Ok(())
}
Self::CannotCalculateChanges { description } => {
write!(f, "JMAP cannotCalculateChanges")?;
if let Some(d) = description {
write!(f, ": {d}")?;
}
Ok(())
}
Self::Unknown => write!(f, "JMAP unknown error"),
}
}
}
impl Error for JmapMethodError {}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JmapSetError {
pub r#type: String,
pub description: Option<String>,
#[serde(default)]
pub properties: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum JmapFilter<C> {
Operator(JmapFilterOperator<C>),
Condition(C),
}
impl<C> From<C> for JmapFilter<C> {
fn from(condition: C) -> Self {
JmapFilter::Condition(condition)
}
}
impl<C> JmapFilter<C> {
pub fn and(conditions: Vec<JmapFilter<C>>) -> Self {
JmapFilter::Operator(JmapFilterOperator {
operator: JmapFilterOperatorKind::And,
conditions,
})
}
pub fn or(conditions: Vec<JmapFilter<C>>) -> Self {
JmapFilter::Operator(JmapFilterOperator {
operator: JmapFilterOperatorKind::Or,
conditions,
})
}
pub fn not(conditions: Vec<JmapFilter<C>>) -> Self {
JmapFilter::Operator(JmapFilterOperator {
operator: JmapFilterOperatorKind::Not,
conditions,
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JmapFilterOperator<C> {
pub operator: JmapFilterOperatorKind,
pub conditions: Vec<JmapFilter<C>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum JmapFilterOperatorKind {
And,
Or,
Not,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JmapResultReference<'a> {
pub result_of: &'a str,
pub name: &'static str,
pub path: &'static str,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JmapRequest {
pub using: Vec<String>,
pub method_calls: Vec<(String, Value, String)>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_ids: Option<BTreeMap<String, String>>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JmapResponse {
pub method_responses: Vec<(String, Value, String)>,
#[serde(default)]
pub created_ids: Option<BTreeMap<String, String>>,
pub session_state: String,
}
#[derive(Debug, Default)]
pub struct JmapBatch {
calls: Vec<(String, Value, String)>,
counter: usize,
}
impl JmapBatch {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, method: impl Into<String>, args: Value) -> String {
let call_id = format!("c{}", self.counter);
self.counter += 1;
self.calls.push((method.into(), args, call_id.clone()));
call_id
}
pub fn into_request(self, using: Vec<String>) -> JmapRequest {
JmapRequest {
using,
method_calls: self.calls,
created_ids: None,
}
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct JmapAddedItem {
pub id: String,
pub index: u64,
}
#[cfg(test)]
mod tests {
use alloc::vec;
use serde_json::json;
use super::*;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
struct Cond {
from: Option<String>,
}
#[test]
fn condition_serializes_flat() {
let f: JmapFilter<Cond> = JmapFilter::Condition(Cond {
from: Some("alice".into()),
});
assert_eq!(
serde_json::to_value(&f).unwrap(),
json!({ "from": "alice" })
);
}
#[test]
fn and_serializes_with_operator_key() {
let f: JmapFilter<Cond> = JmapFilter::and(vec![
JmapFilter::Condition(Cond {
from: Some("a".into()),
}),
JmapFilter::Condition(Cond {
from: Some("b".into()),
}),
]);
assert_eq!(
serde_json::to_value(&f).unwrap(),
json!({
"operator": "AND",
"conditions": [
{ "from": "a" },
{ "from": "b" },
],
}),
);
}
#[test]
fn not_wraps_a_single_subfilter() {
let f: JmapFilter<Cond> = JmapFilter::not(vec![JmapFilter::Condition(Cond {
from: Some("a".into()),
})]);
assert_eq!(
serde_json::to_value(&f).unwrap(),
json!({
"operator": "NOT",
"conditions": [{ "from": "a" }],
}),
);
}
#[test]
fn deserialize_discriminates_on_operator_key() {
let v = json!({ "from": "alice" });
let f: JmapFilter<Cond> = serde_json::from_value(v).unwrap();
assert!(matches!(f, JmapFilter::Condition(Cond { from: Some(_) })));
let v = json!({
"operator": "OR",
"conditions": [{ "from": "a" }, { "from": "b" }],
});
let f: JmapFilter<Cond> = serde_json::from_value(v).unwrap();
assert!(matches!(
f,
JmapFilter::Operator(JmapFilterOperator {
operator: JmapFilterOperatorKind::Or,
..
})
));
}
}