use std::collections::BTreeMap;
use super::{ResumeStart, Trigger, WatchMode};
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct WatchRequest {
event_type: String,
filter: BTreeMap<String, serde_json::Value>,
from: Option<ResumeStart>,
mode: WatchMode,
triggers: Vec<Trigger>,
}
impl WatchRequest {
#[must_use]
pub fn watch(event_type: impl Into<String>) -> Self {
Self {
event_type: event_type.into(),
filter: BTreeMap::new(),
from: None,
mode: WatchMode::Watch,
triggers: Vec::new(),
}
}
#[must_use]
pub fn watch_from(event_type: impl Into<String>, from: ResumeStart) -> Self {
Self {
event_type: event_type.into(),
filter: BTreeMap::new(),
from: Some(from),
mode: WatchMode::Watch,
triggers: Vec::new(),
}
}
#[must_use]
pub fn replay_only(event_type: impl Into<String>, from: ResumeStart) -> Self {
Self {
event_type: event_type.into(),
filter: BTreeMap::new(),
from: Some(from),
mode: WatchMode::ReplayOnly,
triggers: Vec::new(),
}
}
#[must_use]
pub fn with_filter(mut self, filter: BTreeMap<String, serde_json::Value>) -> Self {
self.filter = filter;
self
}
#[must_use]
pub fn event_type(&self) -> &str {
&self.event_type
}
#[must_use]
pub fn filter(&self) -> &BTreeMap<String, serde_json::Value> {
&self.filter
}
#[must_use]
pub fn from(&self) -> Option<&ResumeStart> {
self.from.as_ref()
}
#[must_use]
pub fn mode(&self) -> WatchMode {
self.mode
}
#[must_use]
pub fn with_triggers(mut self, triggers: Vec<Trigger>) -> Self {
self.triggers = triggers;
self
}
#[must_use]
pub fn triggers(&self) -> &[Trigger] {
&self.triggers
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use serde_json::json;
use super::{ResumeStart, WatchMode, WatchRequest};
#[test]
fn watch_constructor_defaults_to_live_watch_mode_with_no_resume() {
let req = WatchRequest::watch("mars");
assert_eq!(req.event_type(), "mars");
assert_eq!(req.mode(), WatchMode::Watch);
assert!(req.from().is_none());
assert!(req.filter().is_empty());
}
#[test]
fn watch_from_carries_resume_position_and_keeps_watch_mode() {
let req = WatchRequest::watch_from("mars", ResumeStart::AfterSequence(41));
assert_eq!(req.event_type(), "mars");
assert_eq!(req.mode(), WatchMode::Watch);
assert_eq!(req.from(), Some(&ResumeStart::AfterSequence(41)));
}
#[test]
fn watch_from_accepts_date_resume_position() {
let req = WatchRequest::watch_from(
"mars",
ResumeStart::Date("2026-01-01T00:00:00Z".to_string()),
);
assert!(matches!(req.from(), Some(ResumeStart::Date(_))));
}
#[test]
fn replay_only_requires_resume_position_and_sets_replay_only_mode() {
let req = WatchRequest::replay_only("mars", ResumeStart::AfterSequence(10));
assert_eq!(req.mode(), WatchMode::ReplayOnly);
assert_eq!(req.from(), Some(&ResumeStart::AfterSequence(10)));
}
#[test]
fn with_filter_replaces_the_filter_map() {
let mut first = BTreeMap::new();
first.insert("country".to_string(), json!("UK"));
let mut second = BTreeMap::new();
second.insert("class".to_string(), json!("od"));
second.insert("stream".to_string(), json!("oper"));
let req = WatchRequest::watch("mars")
.with_filter(first)
.with_filter(second.clone());
assert_eq!(req.filter(), &second);
}
#[test]
fn with_filter_accepts_json_value_constraint_objects() {
let mut filter = BTreeMap::new();
filter.insert("country".to_string(), json!("UK"));
filter.insert(
"polygon".to_string(),
json!({"type": "polygon", "points": [[0, 0], [1, 1]]}),
);
let req = WatchRequest::watch("mars").with_filter(filter);
assert!(req.filter().get("country").is_some());
assert!(
req.filter()
.get("polygon")
.and_then(|v| v.as_object())
.is_some()
);
}
#[test]
fn clone_preserves_all_fields() {
let req = WatchRequest::replay_only("mars", ResumeStart::AfterSequence(7));
let cloned = req.clone();
assert_eq!(cloned.event_type(), req.event_type());
assert_eq!(cloned.mode(), req.mode());
assert_eq!(cloned.from(), req.from());
assert_eq!(cloned.filter(), req.filter());
assert_eq!(cloned.triggers().len(), req.triggers().len());
}
#[test]
fn watch_constructor_starts_with_empty_trigger_list() {
let req = WatchRequest::watch("mars");
assert!(req.triggers().is_empty());
}
#[test]
fn watch_from_constructor_starts_with_empty_trigger_list() {
let req = WatchRequest::watch_from("mars", ResumeStart::AfterSequence(1));
assert!(req.triggers().is_empty());
}
#[test]
fn replay_only_constructor_starts_with_empty_trigger_list() {
let req = WatchRequest::replay_only("mars", ResumeStart::AfterSequence(1));
assert!(req.triggers().is_empty());
}
#[test]
fn with_triggers_replaces_the_trigger_list() {
use crate::watch::Trigger;
let first = vec![Trigger::echo()];
let second = vec![Trigger::echo(), Trigger::log("/tmp/r.log")];
let req = WatchRequest::watch("mars")
.with_triggers(first)
.with_triggers(second.clone());
assert_eq!(req.triggers().len(), second.len());
}
#[test]
fn clone_preserves_attached_triggers() {
use crate::watch::Trigger;
let req = WatchRequest::watch("mars")
.with_triggers(vec![Trigger::log("/tmp/clone.log").required(false)]);
let cloned = req.clone();
assert_eq!(cloned.triggers().len(), 1);
assert!(!cloned.triggers()[0].required);
}
}