use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use jmap_types::Id;
const USING_BLOB: &[&str] = &["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:blob2"];
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BlobLookupEntry {
pub id: Id,
pub matched_ids: HashMap<String, Vec<Id>>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BlobLookupResponse {
pub account_id: Id,
pub list: Vec<BlobLookupEntry>,
#[serde(default)]
pub not_found: Vec<Id>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BlobObject {
pub id: Id,
#[serde(rename = "type")]
pub content_type: Option<String>,
pub size: Option<u64>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BlobConvertResponse {
pub account_id: Id,
#[serde(default)]
pub created: Option<HashMap<String, BlobObject>>,
#[serde(default)]
pub not_created: Option<HashMap<String, super::SetError>>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ImageConvertRecipe<'a> {
blob_id: &'a Id,
#[serde(rename = "type")]
content_type: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
height: Option<u32>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct BlobConvertCreate<'a> {
image_convert: ImageConvertRecipe<'a>,
}
impl super::SessionClient {
pub async fn blob_lookup(
&self,
blob_ids: &[Id],
type_names: Option<&[&str]>,
) -> Result<BlobLookupResponse, jmap_base_client::ClientError> {
if blob_ids.is_empty() {
return Err(jmap_base_client::ClientError::InvalidArgument(
"blob_lookup: blob_ids may not be empty".into(),
));
}
let (api_url, account_id) = self.session_parts()?;
let args = serde_json::json!({
"accountId": account_id,
"ids": blob_ids,
"typeNames": type_names,
});
let req = super::build_request("Blob/lookup", args, USING_BLOB);
let resp = self.call_internal(api_url, &req).await?;
jmap_base_client::extract_response(&resp, super::CALL_ID)
}
pub async fn blob_convert(
&self,
from_blob_id: &Id,
content_type: &str,
width: Option<u32>,
height: Option<u32>,
) -> Result<BlobConvertResponse, jmap_base_client::ClientError> {
if content_type.is_empty() {
return Err(jmap_base_client::ClientError::InvalidArgument(
"blob_convert: content_type may not be empty".into(),
));
}
let (api_url, account_id) = self.session_parts()?;
let create_entry = BlobConvertCreate {
image_convert: ImageConvertRecipe {
blob_id: from_blob_id,
content_type,
width,
height,
},
};
let create_map = {
let mut m = serde_json::Map::new();
m.insert(
super::CALL_ID.to_owned(),
serde_json::to_value(&create_entry)
.map_err(jmap_base_client::ClientError::from_parse)?,
);
m
};
let args = serde_json::json!({
"accountId": account_id,
"create": serde_json::Value::Object(create_map),
});
let req = super::build_request("Blob/convert", args, USING_BLOB);
let resp = self.call_internal(api_url, &req).await?;
jmap_base_client::extract_response(&resp, super::CALL_ID)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn image_convert_recipe_wire_format() {
let blob_id = Id::from("Bxxx");
let recipe = ImageConvertRecipe {
blob_id: &blob_id,
content_type: "image/webp",
width: Some(100),
height: None,
};
let entry = BlobConvertCreate {
image_convert: recipe,
};
let v = serde_json::to_value(&entry).expect("serialization must not fail");
let ic = v.get("imageConvert").expect("must have imageConvert key");
assert_eq!(ic.get("blobId").and_then(|v| v.as_str()), Some("Bxxx"));
assert_eq!(ic.get("type").and_then(|v| v.as_str()), Some("image/webp"));
assert_eq!(ic.get("width").and_then(|v| v.as_u64()), Some(100));
assert!(
ic.get("height").is_none(),
"height must be omitted when None"
);
}
#[test]
fn blob_convert_response_deserialise() {
let json = serde_json::json!({
"accountId": "abc",
"created": {
"r1": {
"id": "Bnew",
"type": "image/webp",
"size": 12345
}
},
"notCreated": {}
});
let resp: BlobConvertResponse =
serde_json::from_value(json).expect("deserialisation must not fail");
assert_eq!(resp.account_id, "abc");
let created = resp.created.expect("created must be Some");
let obj = created.get("r1").expect("r1 key must be present");
assert_eq!(obj.id.as_ref(), "Bnew");
assert_eq!(obj.content_type.as_deref(), Some("image/webp"));
assert_eq!(obj.size, Some(12345));
}
#[test]
fn blob_lookup_entry_preserves_vendor_extras() {
let raw = serde_json::json!({
"id": "B1",
"matchedIds": {
"Message": ["M1", "M2"]
},
"acmeCorpCacheHit": true
});
let obj: BlobLookupEntry =
serde_json::from_value(raw).expect("BlobLookupEntry must deserialize");
assert_eq!(
obj.extra.get("acmeCorpCacheHit").and_then(|v| v.as_bool()),
Some(true)
);
}
#[test]
fn blob_lookup_response_preserves_vendor_extras() {
let raw = serde_json::json!({
"accountId": "acc1",
"list": [],
"acmeCorpRequestId": "req-42"
});
let obj: BlobLookupResponse =
serde_json::from_value(raw).expect("BlobLookupResponse must deserialize");
assert_eq!(
obj.extra.get("acmeCorpRequestId").and_then(|v| v.as_str()),
Some("req-42")
);
}
#[test]
fn blob_object_preserves_vendor_extras() {
let raw = serde_json::json!({
"id": "Bnew",
"type": "image/webp",
"size": 12345,
"acmeCorpCdnUrl": "https://cdn.example.com/Bnew"
});
let obj: BlobObject = serde_json::from_value(raw).expect("BlobObject must deserialize");
assert_eq!(
obj.extra.get("acmeCorpCdnUrl").and_then(|v| v.as_str()),
Some("https://cdn.example.com/Bnew")
);
}
#[test]
fn blob_convert_response_preserves_vendor_extras() {
let raw = serde_json::json!({
"accountId": "acc1",
"created": {},
"notCreated": {},
"acmeCorpJobId": "job-7"
});
let obj: BlobConvertResponse =
serde_json::from_value(raw).expect("BlobConvertResponse must deserialize");
assert_eq!(
obj.extra.get("acmeCorpJobId").and_then(|v| v.as_str()),
Some("job-7")
);
}
}