pub mod task;
pub mod task_list;
pub mod task_notification;
pub use jmap_types::{
AddedItem, ChangesResponse, GetResponse, QueryChangesResponse, QueryResponse, SetError,
SetResponse,
};
pub(crate) const CALL_ID: &str = "r1";
pub(crate) const USING_TASKS: &[&str] =
&["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:tasks"];
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(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("urn:ietf:params:jmap:tasks")
.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
}
}
#[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()));
}
}