pub mod task;
pub mod task_list;
pub mod task_notification;
pub use jmap_types::{
AddedItem, ChangesResponse, GetResponse, QueryChangesResponse, QueryResponse, SetError,
SetResponse,
};
#[derive(Debug, Default, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskListSetParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub on_destroy_remove_tasks: Option<bool>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
pub(crate) const CALL_ID: &str = "r1";
pub(crate) const USING_TASKS: &[&str] = &[
"urn:ietf:params:jmap:core",
jmap_tasks_types::JMAP_TASKS_URI,
];
pub(crate) fn build_request(
method: &str,
args: serde_json::Value,
using: &[&str],
) -> jmap_types::JmapRequest {
let using_vec: Vec<String> = using.iter().map(|&s| s.to_owned()).collect();
let invocation: jmap_types::Invocation = (method.to_owned(), args, CALL_ID.to_owned());
jmap_types::JmapRequest::new(using_vec, vec![invocation], None)
}
#[non_exhaustive]
#[derive(Clone)]
pub struct SessionClient {
pub(crate) client: jmap_base_client::JmapClient,
pub(crate) session: jmap_base_client::Session,
}
impl std::fmt::Debug for SessionClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SessionClient")
.field("client", &"<JmapClient>")
.field("session", &self.session)
.finish()
}
}
impl SessionClient {
pub fn client(&self) -> &jmap_base_client::JmapClient {
&self.client
}
pub fn session(&self) -> &jmap_base_client::Session {
&self.session
}
pub fn tasks_account_id(&self) -> Result<&str, jmap_base_client::ClientError> {
self.session
.primary_account_id(jmap_tasks_types::JMAP_TASKS_URI)
.ok_or_else(|| {
jmap_base_client::ClientError::InvalidSession(
"no primary account for urn:ietf:params:jmap:tasks".into(),
)
})
}
pub(crate) fn session_parts(&self) -> Result<(&str, &str), jmap_base_client::ClientError> {
let api_url = self.session.api_url.as_str();
let account_id = self
.session
.primary_account_id(jmap_tasks_types::JMAP_TASKS_URI)
.ok_or_else(|| {
jmap_base_client::ClientError::InvalidSession(
"no primary account for urn:ietf:params:jmap:tasks".into(),
)
})?;
Ok((api_url, account_id))
}
pub(crate) async fn call_internal(
&self,
api_url: &str,
req: &jmap_types::JmapRequest,
) -> Result<jmap_types::JmapResponse, jmap_base_client::ClientError> {
self.client.call(api_url, req).await
}
}
#[allow(dead_code)]
fn _assert_session_client_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<SessionClient>();
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn build_request_method_name_and_call_id() {
let req = build_request(
"TaskList/get",
json!({"accountId": "acc1", "ids": null}),
USING_TASKS,
);
let v = serde_json::to_value(&req).expect("serialize JmapRequest");
let calls = v["methodCalls"]
.as_array()
.expect("methodCalls must be array");
assert_eq!(calls.len(), 1, "must have exactly 1 method call");
assert_eq!(calls[0][0], json!("TaskList/get"), "method name must match");
assert_eq!(calls[0][2], json!("r1"), "call_id must be CALL_ID constant");
}
#[test]
fn using_tasks_contains_correct_uris() {
let req = build_request("TaskList/get", json!({}), USING_TASKS);
let v = serde_json::to_value(&req).expect("serialize");
let using = v["using"].as_array().expect("using must be array");
assert_eq!(using.len(), 2);
assert!(
using.contains(&json!("urn:ietf:params:jmap:core")),
"must include jmap:core"
);
assert!(
using.contains(&json!("urn:ietf:params:jmap:tasks")),
"must include jmap:tasks"
);
}
#[test]
fn call_id_is_r1() {
assert_eq!(CALL_ID, "r1");
}
#[test]
fn get_response_deserializes() {
let json = json!({
"accountId": "acc1",
"state": "s42",
"list": [],
"notFound": ["missing1"]
});
let resp: GetResponse<serde_json::Value> =
serde_json::from_value(json).expect("GetResponse must deserialize");
assert_eq!(resp.account_id, "acc1");
assert_eq!(resp.state, "s42");
assert!(resp.list.is_empty());
assert_eq!(
resp.not_found.as_deref(),
Some(["missing1".into()].as_slice())
);
}
#[test]
fn set_response_deserializes() {
let json = json!({
"accountId": "acc1",
"oldState": "s10",
"newState": "s11",
"created": null,
"updated": null,
"destroyed": ["id1"],
"notCreated": null,
"notUpdated": null,
"notDestroyed": null
});
let resp: SetResponse = serde_json::from_value(json).expect("SetResponse must deserialize");
assert_eq!(resp.new_state, "s11");
assert_eq!(resp.destroyed.as_deref(), Some(["id1".into()].as_slice()));
}
#[test]
fn task_list_set_params_on_destroy_remove_tasks_serializes() {
let params = TaskListSetParams {
on_destroy_remove_tasks: Some(true),
extra: serde_json::Map::new(),
};
let out = serde_json::to_value(¶ms).expect("serialize TaskListSetParams");
assert_eq!(out["onDestroyRemoveTasks"], json!(true));
}
#[test]
fn task_list_set_params_default_serializes_empty() {
let params = TaskListSetParams::default();
let out = serde_json::to_value(¶ms).expect("serialize TaskListSetParams::default");
let obj = out.as_object().expect("must be Object");
assert!(
obj.is_empty(),
"all-None default must serialize to empty object, got: {out}"
);
}
#[test]
fn task_list_set_params_propagates_vendor_extras() {
let mut params = TaskListSetParams::default();
params
.extra
.insert("acmeCorpCascade".into(), json!("strict"));
let v = serde_json::to_value(¶ms).expect("serialize TaskListSetParams");
assert_eq!(v["acmeCorpCascade"], json!("strict"));
}
}