use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)]
pub struct PollRequest<'a> {
pub trigger: &'a str,
pub connector: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<&'a str>,
pub max_items: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<&'a serde_json::Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct PollItem {
pub id: String,
pub payload: serde_json::Value,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub blobs: Vec<PollBlob>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct PollBlob {
pub filename: Option<String>,
pub mime: Option<String>,
pub b64: String,
}
pub const MAX_BLOB_BYTES_PER_ITEM: usize = 16 * 1024 * 1024;
pub const MAX_BLOB_BYTES_PER_RESPONSE: usize = 48 * 1024 * 1024;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct PollResponse {
pub items: Vec<PollItem>,
pub cursor: Option<String>,
pub more: bool,
}
pub fn dedup_value(item: &PollItem, pointers: &[String]) -> Option<String> {
if pointers.is_empty() {
return if item.id.trim().is_empty() { None } else { Some(item.id.clone()) };
}
let mut parts = Vec::with_capacity(pointers.len());
for p in pointers {
let v = item.payload.pointer(p)?;
parts.push(match v {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
});
}
Some(parts.join("\u{1f}"))
}
pub fn rewrite_blob_refs(payload: &mut serde_json::Value, uris: &[String]) -> Result<(), String> {
match payload {
serde_json::Value::Object(map) => {
for (k, v) in map.iter_mut() {
if k == "blob" {
if let serde_json::Value::String(s) = v {
if let Some(n) = s.strip_prefix('@') {
let uri = n
.parse::<usize>()
.ok()
.and_then(|i| uris.get(i))
.ok_or_else(|| {
format!(
"payload references blob \"@{n}\" but the item \
carries {} blob(s)",
uris.len()
)
})?;
*v = serde_json::Value::String(uri.clone());
continue;
}
}
}
rewrite_blob_refs(v, uris)?;
}
Ok(())
}
serde_json::Value::Array(arr) => {
for v in arr.iter_mut() {
rewrite_blob_refs(v, uris)?;
}
Ok(())
}
_ => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn item(id: &str, payload: serde_json::Value) -> PollItem {
PollItem { id: id.into(), payload, blobs: Vec::new() }
}
#[test]
fn falls_back_to_the_connectors_own_id() {
let i = item("abc", serde_json::json!({}));
assert_eq!(dedup_value(&i, &[]), Some("abc".into()));
}
#[test]
fn an_item_with_no_identity_at_all_yields_none() {
let i = item("", serde_json::json!({}));
assert_eq!(dedup_value(&i, &[]), None);
assert_eq!(dedup_value(&i, &["/missing".into()]), None);
}
#[test]
fn a_declared_pointer_beats_the_connectors_id() {
let i = item("connector-chosen", serde_json::json!({ "message_id": "m1" }));
assert_eq!(dedup_value(&i, &["/message_id".into()]), Some("m1".into()));
}
#[test]
fn composite_pointers_identify_the_occurrence_not_the_entity() {
let pointers = vec!["/id".to_string(), "/updated_at".to_string()];
let first = item("x", serde_json::json!({ "id": "42", "updated_at": 100 }));
let edited = item("x", serde_json::json!({ "id": "42", "updated_at": 200 }));
let a = dedup_value(&first, &pointers).unwrap();
let b = dedup_value(&edited, &pointers).unwrap();
assert_ne!(a, b, "an edit is a new occurrence of the same entity");
}
#[test]
fn the_joiner_cannot_be_forged_from_field_contents() {
let pointers = vec!["/a".to_string(), "/b".to_string()];
let x = item("", serde_json::json!({ "a": "1-2", "b": "3" }));
let y = item("", serde_json::json!({ "a": "1", "b": "2-3" }));
assert_ne!(dedup_value(&x, &pointers), dedup_value(&y, &pointers));
}
#[test]
fn absent_cursor_in_a_response_means_leave_it_alone() {
let r: PollResponse = serde_json::from_str(r#"{"items":[]}"#).unwrap();
assert!(r.cursor.is_none(), "not Some(\"null\") and not an error");
assert!(!r.more);
}
#[test]
fn blob_refs_rewrite_to_cas_addresses() {
let uris = vec!["cas://sha256:aa".to_string(), "cas://sha256:bb".to_string()];
let mut payload = serde_json::json!({
"email": { "from": "a@b.c" },
"attachments": [
{ "filename": "inv.pdf", "blob": "@0" },
{ "filename": "po.pdf", "blob": "@1" },
],
"note": "@0",
"prior": { "blob": "cas://sha256:cc" },
});
rewrite_blob_refs(&mut payload, &uris).unwrap();
assert_eq!(payload["attachments"][0]["blob"], "cas://sha256:aa");
assert_eq!(payload["attachments"][1]["blob"], "cas://sha256:bb");
assert_eq!(payload["note"], "@0");
assert_eq!(payload["prior"]["blob"], "cas://sha256:cc");
}
#[test]
fn a_dangling_blob_ref_is_refused_not_left_in_place() {
let uris = vec!["cas://sha256:aa".to_string()];
let mut payload = serde_json::json!({ "a": { "blob": "@7" } });
assert!(rewrite_blob_refs(&mut payload, &uris).is_err());
let mut payload = serde_json::json!({ "a": { "blob": "@zero" } });
assert!(rewrite_blob_refs(&mut payload, &uris).is_err());
}
#[test]
fn blobs_parse_and_default_to_empty() {
let r: PollResponse = serde_json::from_str(
r#"{"items":[{"id":"m1","payload":{"a":{"blob":"@0"}},
"blobs":[{"filename":"inv.pdf","mime":"application/pdf","b64":"Zm9v"}]}]}"#,
)
.unwrap();
assert_eq!(r.items[0].blobs.len(), 1);
assert_eq!(r.items[0].blobs[0].filename.as_deref(), Some("inv.pdf"));
let r: PollResponse =
serde_json::from_str(r#"{"items":[{"id":"m1","payload":{}}]}"#).unwrap();
assert!(r.items[0].blobs.is_empty());
}
#[test]
fn unknown_response_fields_are_ignored() {
let r: PollResponse =
serde_json::from_str(r#"{"items":[],"cursor":"c","more":true,"future":1}"#).unwrap();
assert_eq!(r.cursor.as_deref(), Some("c"));
assert!(r.more);
}
}