pub mod addressbook;
pub mod card;
pub use jmap_types::{
AddedItem, ChangesResponse, GetResponse, QueryChangesResponse, QueryResponse, SetError,
SetResponse,
};
#[derive(Debug, Default, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AddressBookSetParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub on_destroy_remove_contents: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub on_success_set_is_default: Option<serde_json::Value>,
#[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_CONTACTS: &[&str] = &[
"urn:ietf:params:jmap:core",
jmap_contacts_types::JMAP_CONTACTS_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 contacts_account_id(&self) -> Result<&str, jmap_base_client::ClientError> {
self.session
.primary_account_id(jmap_contacts_types::JMAP_CONTACTS_URI)
.ok_or_else(|| {
jmap_base_client::ClientError::InvalidSession(
"no primary account for urn:ietf:params:jmap:contacts".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_contacts_types::JMAP_CONTACTS_URI)
.ok_or_else(|| {
jmap_base_client::ClientError::InvalidSession(
"no primary account for urn:ietf:params:jmap:contacts".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 using_contacts_contains_correct_uris() {
let req = build_request("AddressBook/get", json!({}), USING_CONTACTS);
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, "must have exactly 2 capability URIs");
assert!(
using.contains(&json!("urn:ietf:params:jmap:core")),
"must include jmap:core"
);
assert!(
using.contains(&json!("urn:ietf:params:jmap:contacts")),
"must include jmap:contacts"
);
}
#[test]
fn build_request_method_name_and_call_id() {
let req = build_request(
"AddressBook/get",
json!({"accountId": "acc1", "ids": null}),
USING_CONTACTS,
);
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!("AddressBook/get"),
"method name must match"
);
assert_eq!(calls[0][2], json!("r1"), "call_id must be CALL_ID constant");
}
#[test]
fn address_book_set_params_serializes_on_destroy_remove_contents() {
let params = AddressBookSetParams {
on_destroy_remove_contents: Some(true),
on_success_set_is_default: None,
extra: serde_json::Map::new(),
};
let v = serde_json::to_value(¶ms).expect("serialize");
assert_eq!(
v["onDestroyRemoveContents"],
json!(true),
"onDestroyRemoveContents must be present and true"
);
assert!(
v.get("onSuccessSetIsDefault").is_none(),
"onSuccessSetIsDefault must be absent when None"
);
}
#[test]
fn address_book_set_params_default_is_empty_object() {
let params = AddressBookSetParams::default();
let v = serde_json::to_value(¶ms).expect("serialize");
assert_eq!(
v,
json!({}),
"default params must serialize to empty object"
);
}
#[test]
fn address_book_set_params_serializes_on_success_set_is_default() {
let params = AddressBookSetParams {
on_destroy_remove_contents: None,
on_success_set_is_default: Some(json!({"newDefaultId": true})),
extra: serde_json::Map::new(),
};
let v = serde_json::to_value(¶ms).expect("serialize");
assert!(
v.get("onDestroyRemoveContents").is_none(),
"onDestroyRemoveContents must be absent when None"
);
assert_eq!(
v["onSuccessSetIsDefault"],
json!({"newDefaultId": true}),
"onSuccessSetIsDefault must be present"
);
}
#[test]
fn session_parts_err_no_primary_account() {
let session_json = json!({
"capabilities": {},
"accounts": {},
"primaryAccounts": {},
"username": "user@example.com",
"apiUrl": "https://jmap.example.com/api/",
"downloadUrl": "https://jmap.example.com/dl/{accountId}/{blobId}/{name}?accept={type}",
"uploadUrl": "https://jmap.example.com/ul/{accountId}/",
"eventSourceUrl": "https://jmap.example.com/sse/?types={types}&closeafter={closeafter}&ping={ping}",
"state": "s1"
});
let session: jmap_base_client::Session =
serde_json::from_value(session_json).expect("session must deserialize");
let result = session.primary_account_id("urn:ietf:params:jmap:contacts");
assert!(
result.is_none(),
"must return None when contacts capability is not in primaryAccounts"
);
}
#[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 changes_response_deserializes() {
let json = json!({
"accountId": "acc1",
"oldState": "s10",
"newState": "s11",
"hasMoreChanges": false,
"created": ["id1"],
"updated": ["id2"],
"destroyed": []
});
let resp: ChangesResponse =
serde_json::from_value(json).expect("ChangesResponse must deserialize");
assert_eq!(resp.old_state, "s10");
assert_eq!(resp.new_state, "s11");
assert!(!resp.has_more_changes);
assert_eq!(resp.created.len(), 1);
assert_eq!(resp.updated.len(), 1);
assert!(resp.destroyed.is_empty());
}
#[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 query_changes_response_deserializes() {
let json = json!({
"accountId": "acc1",
"oldQueryState": "qs1",
"newQueryState": "qs2",
"total": 5,
"removed": ["id3"],
"added": [{"id": "id4", "index": 0}]
});
let resp: QueryChangesResponse =
serde_json::from_value(json).expect("QueryChangesResponse must deserialize");
assert_eq!(resp.old_query_state, "qs1");
assert_eq!(resp.new_query_state, "qs2");
assert_eq!(resp.total, Some(5));
assert_eq!(resp.removed.len(), 1);
assert_eq!(resp.added.len(), 1);
assert_eq!(resp.added[0].index, 0);
}
#[test]
fn address_book_set_params_propagates_vendor_extras() {
let mut params = AddressBookSetParams::default();
params
.extra
.insert("acmeCorpCascade".into(), json!("strict"));
let v = serde_json::to_value(¶ms).expect("serialize AddressBookSetParams");
assert_eq!(v["acmeCorpCascade"], json!("strict"));
}
}