use std::pin::Pin;
use async_trait::async_trait;
use futures::Stream;
use jiff::Timestamp;
use serde::{Deserialize, Serialize};
use crate::Result;
pub type WatchStream = Pin<Box<dyn Stream<Item = Result<WatchFrame>> + Send + 'static>>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WatchRequest {
pub bucket: Option<String>,
pub events: Vec<String>,
pub prefix: Option<String>,
pub suffix: Option<String>,
pub ping_seconds: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WatchFrame {
KeepAlive,
Event(Box<WatchEvent>),
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct WatchSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub event_source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub region: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub principal_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub host: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub port: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_agent: Option<String>,
}
impl WatchSource {
pub fn is_empty(&self) -> bool {
self.event_source.is_none()
&& self.region.is_none()
&& self.principal_id.is_none()
&& self.host.is_none()
&& self.port.is_none()
&& self.user_agent.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WatchEvent {
pub event_id: Option<String>,
pub event_name: String,
pub bucket: String,
pub key: String,
pub version_id: Option<String>,
pub delete_marker: bool,
pub size_bytes: Option<i64>,
pub etag: Option<String>,
pub event_time: Timestamp,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<WatchSource>,
}
#[async_trait]
pub trait WatchApi: Send + Sync {
async fn watch(&self, request: &WatchRequest) -> Result<WatchStream>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_metadata_detects_empty_and_populated_values() {
assert!(WatchSource::default().is_empty());
let source = WatchSource {
host: Some("node-1".to_string()),
..WatchSource::default()
};
assert!(!source.is_empty());
}
#[test]
fn watch_event_serialization_is_independent_of_object_version_operations() {
let event = WatchEvent {
event_id: Some("sequence-1".to_string()),
event_name: "s3:ObjectRemoved:DeleteMarkerCreated".to_string(),
bucket: "photos".to_string(),
key: "old.jpg".to_string(),
version_id: Some("v2".to_string()),
delete_marker: true,
size_bytes: None,
etag: None,
event_time: "2026-07-21T04:01:00Z"
.parse()
.expect("timestamp should parse"),
source: None,
};
let value = serde_json::to_value(event).expect("event should serialize");
assert_eq!(value["delete_marker"], true);
assert_eq!(value["version_id"], "v2");
assert!(value.get("is_latest").is_none());
}
}