use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PubsubBookmark {
#[serde(default)]
pub delivered: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_message_id: Option<String>,
}
impl PubsubBookmark {
pub fn advance(&mut self, message_id: &str) {
self.delivered += 1;
self.last_message_id = Some(message_id.to_string());
}
pub fn to_value(&self) -> Value {
serde_json::to_value(self).unwrap_or(Value::Null)
}
pub fn from_value(v: &Value) -> Self {
serde_json::from_value(v.clone()).unwrap_or_default()
}
}
pub fn state_key(subscription: &str) -> String {
format!("pubsub:{subscription}")
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn bookmark_advances_and_round_trips() {
let mut b = PubsubBookmark::default();
b.advance("m1");
b.advance("m2");
assert_eq!(b.delivered, 2);
assert_eq!(b.last_message_id.as_deref(), Some("m2"));
let back = PubsubBookmark::from_value(&b.to_value());
assert_eq!(back, b);
}
#[test]
fn malformed_bookmark_is_fresh() {
assert_eq!(
PubsubBookmark::from_value(&json!("nope")),
PubsubBookmark::default()
);
assert_eq!(
PubsubBookmark::from_value(&json!(null)),
PubsubBookmark::default()
);
}
#[test]
fn state_key_shape_is_valid() {
assert_eq!(state_key("orders-sub"), "pubsub:orders-sub");
faucet_core::state::validate_state_key(&state_key("orders-sub")).unwrap();
}
}