pub mod metadata;
pub use jmap_types::{
AddedItem, ChangesResponse, GetResponse, QueryChangesResponse, QueryResponse, SetError,
SetResponse,
};
#[derive(Debug, Default, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MetadataChangesParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub filter_related_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filter_metadata_type: Option<Vec<String>>,
#[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_METADATA: &[&str] = &[
"urn:ietf:params:jmap:core",
jmap_metadata_types::JMAP_METADATA_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(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_metadata_types::JMAP_METADATA_URI)
.ok_or_else(|| {
jmap_base_client::ClientError::InvalidSession(
"no primary account for urn:ietf:params:jmap:metadata".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 using_metadata_contains_correct_uris() {
let req = build_request("Metadata/get", json!({}), USING_METADATA);
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:metadata")),
"must include jmap:metadata"
);
}
#[test]
fn build_request_method_name_and_call_id() {
let req = build_request(
"Metadata/get",
json!({"accountId": "acc1", "ids": null}),
USING_METADATA,
);
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!("Metadata/get"), "method name must match");
assert_eq!(calls[0][2], json!("r1"), "call_id must be CALL_ID constant");
}
#[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:metadata");
assert!(
result.is_none(),
"must return None when metadata 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 metadata_changes_params_default_is_empty_object() {
let params = MetadataChangesParams::default();
let v = serde_json::to_value(¶ms).expect("serialize");
assert_eq!(
v,
json!({}),
"default params must serialize to empty object"
);
}
#[test]
fn metadata_changes_params_serializes_filter_related_type() {
let params = MetadataChangesParams {
filter_related_type: Some("Email".into()),
filter_metadata_type: None,
extra: serde_json::Map::new(),
};
let v = serde_json::to_value(¶ms).expect("serialize");
assert_eq!(
v["filterRelatedType"],
json!("Email"),
"filterRelatedType must serialize in camelCase"
);
assert!(
v.get("filterMetadataType").is_none(),
"filterMetadataType must be absent when None"
);
}
#[test]
fn metadata_changes_params_serializes_filter_metadata_type() {
let params = MetadataChangesParams {
filter_related_type: None,
filter_metadata_type: Some(vec!["Annotation".into(), "ImapMetadata".into()]),
extra: serde_json::Map::new(),
};
let v = serde_json::to_value(¶ms).expect("serialize");
assert!(
v.get("filterRelatedType").is_none(),
"filterRelatedType must be absent when None"
);
assert_eq!(
v["filterMetadataType"],
json!(["Annotation", "ImapMetadata"]),
"filterMetadataType must serialize as array"
);
}
#[test]
fn metadata_changes_params_propagates_vendor_extras() {
let mut params = MetadataChangesParams::default();
params
.extra
.insert("acmeCorpCursor".into(), json!("opaque-token"));
let v = serde_json::to_value(¶ms).expect("serialize MetadataChangesParams");
assert_eq!(v["acmeCorpCursor"], json!("opaque-token"));
}
}