use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct CallError {
pub name: String,
pub message: String,
}
#[derive(Debug, Clone)]
pub enum CallOutcome {
Ok {
value: Option<serde_json::Value>,
text: Option<String>,
},
Err(CallError),
}
impl CallOutcome {
pub fn is_ok(&self) -> bool {
matches!(self, CallOutcome::Ok { .. })
}
}
#[derive(Debug, Clone)]
pub struct CallAnswer {
pub origin: String,
pub outcome: CallOutcome,
pub attachment: Option<serde_json::Value>,
pub attachment_bytes: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageSignal {
pub partial: bool,
pub next_cursor: Option<String>,
pub scanned: Option<u64>,
pub covers_from: Option<String>,
}
impl PageSignal {
pub fn is_contract_violation(&self) -> bool {
self.partial && self.next_cursor.is_none()
}
}
impl CallAnswer {
pub fn page_signal(&self) -> Option<PageSignal> {
let CallOutcome::Ok {
value: Some(serde_json::Value::Object(o)),
..
} = &self.outcome
else {
return None;
};
let partial = o.get("partial")?.as_bool()?;
Some(PageSignal {
partial,
next_cursor: o
.get("next_cursor")
.and_then(|c| c.as_str())
.map(str::to_string),
scanned: o.get("scanned").and_then(|n| n.as_u64()),
covers_from: o
.get("covers_from")
.and_then(|c| c.as_str())
.map(str::to_string),
})
}
}
impl Serialize for CallAnswer {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let mut m = serializer.serialize_map(None)?;
m.serialize_entry("origin", &self.origin)?;
m.serialize_entry("ok", &self.outcome.is_ok())?;
if let CallOutcome::Ok { value, text } = &self.outcome {
if let Some(v) = value {
m.serialize_entry("value", v)?;
}
if let Some(t) = text {
m.serialize_entry("text", t)?;
}
}
if let Some(a) = &self.attachment {
m.serialize_entry("attachment", a)?;
}
if let Some(n) = self.attachment_bytes {
m.serialize_entry("attachment_bytes", &n)?;
}
if let CallOutcome::Err(e) = &self.outcome {
m.serialize_entry("error", e)?;
}
m.end()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct CallReport {
pub key: String,
pub timeout_s: f64,
pub answers: Vec<CallAnswer>,
}
impl CallReport {
pub fn exit_code(&self) -> i32 {
if self.answers.is_empty() {
2
} else if self
.answers
.iter()
.any(|a| matches!(a.outcome, CallOutcome::Err(_)))
{
1
} else {
0
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ProbeReport {
pub input: String,
pub origin: String,
pub via: String,
pub call: CallReport,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ValueSource {
Storage,
Cache,
Window,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn call_exit_codes() {
let mut r = CallReport {
key: "k".into(),
timeout_s: 5.0,
answers: vec![],
};
assert_eq!(r.exit_code(), 2, "silence is its own exit code");
r.answers.push(CallAnswer {
origin: "h-1".into(),
outcome: CallOutcome::Ok {
value: None,
text: Some("x".into()),
},
attachment: None,
attachment_bytes: None,
});
assert_eq!(r.exit_code(), 0);
r.answers.push(CallAnswer {
origin: "h-2".into(),
outcome: CallOutcome::Err(CallError {
name: "error/busy".into(),
message: "later".into(),
}),
attachment: None,
attachment_bytes: None,
});
assert_eq!(r.exit_code(), 1, "any refusal fails the invocation");
}
fn answer(outcome: CallOutcome) -> CallAnswer {
CallAnswer {
origin: "h-1".into(),
outcome,
attachment: None,
attachment_bytes: None,
}
}
fn value(v: serde_json::Value) -> CallAnswer {
answer(CallOutcome::Ok {
value: Some(v),
text: None,
})
}
#[test]
fn page_signal_reads_only_object_replies_with_partial() {
let full = value(serde_json::json!({
"items": [1, 2],
"next_cursor": "k-2",
"partial": true,
"scanned": 4096,
"covers_from": "2026-09-06T10:00:00Z"
}));
assert_eq!(
full.page_signal(),
Some(PageSignal {
partial: true,
next_cursor: Some("k-2".into()),
scanned: Some(4096),
covers_from: Some("2026-09-06T10:00:00Z".into()),
})
);
assert!(!full.page_signal().unwrap().is_contract_violation());
let stuck = value(serde_json::json!({"items": [], "next_cursor": null, "partial": true}));
let p = stuck
.page_signal()
.expect("an object with a boolean partial");
assert!(p.is_contract_violation());
assert_eq!((p.scanned, p.covers_from), (None, None));
let done = value(serde_json::json!({"items": [], "next_cursor": null, "partial": false}));
assert!(!done.page_signal().unwrap().is_contract_violation());
for a in [
value(serde_json::json!([1, 2, 3])),
value(serde_json::json!(42)),
value(serde_json::json!({"count": 214})),
value(serde_json::json!({"partial": "yes"})),
answer(CallOutcome::Ok {
value: None,
text: Some("partial = true".into()),
}),
answer(CallOutcome::Err(CallError {
name: "error/busy".into(),
message: "later".into(),
})),
] {
assert_eq!(a.page_signal(), None, "{a:?}");
}
}
}