use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub type WatchId = String;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Watch {
pub id: WatchId,
pub url: String,
pub selector: Option<String>,
pub interval_secs: u64,
pub created_at: DateTime<Utc>,
pub last_check_at: Option<DateTime<Utc>>,
pub last_change_at: Option<DateTime<Utc>>,
pub last_etag: Option<String>,
pub last_last_modified: Option<String>,
pub snapshots: Vec<WatchSnapshot>,
pub consecutive_errors: u32,
pub options: WatchOptions,
}
impl Watch {
pub fn is_due(&self) -> bool {
if self.interval_secs == 0 {
return false;
}
match self.last_check_at {
None => true,
Some(last) => {
let elapsed =
u64::try_from(Utc::now().signed_duration_since(last).num_seconds().max(0))
.unwrap_or(0);
elapsed >= self.interval_secs
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchSnapshot {
pub sha256: String,
pub captured_at: DateTime<Utc>,
pub size: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WatchOptions {
#[serde(default = "default_notify_on")]
pub notify_on: NotifyOn,
#[serde(default)]
pub diff_kind: DiffKind,
#[serde(default = "default_max_snapshots")]
pub max_snapshots: usize,
}
impl Default for WatchOptions {
fn default() -> Self {
Self {
notify_on: default_notify_on(),
diff_kind: DiffKind::default(),
max_snapshots: default_max_snapshots(),
}
}
}
fn default_notify_on() -> NotifyOn {
NotifyOn::Any
}
fn default_max_snapshots() -> usize {
10
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum NotifyOn {
#[default]
Any,
Regression,
Semantic,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum DiffKind {
#[default]
Text,
Semantic,
Dom,
}
#[derive(Debug, Clone)]
pub enum WatchEvent {
Added(WatchId),
Removed(WatchId),
Checked { id: WatchId, changed: bool },
Changed { id: WatchId, summary: String },
Error { id: WatchId, error: String },
}
#[derive(Debug, Clone, Default)]
pub struct AddOptions {
pub selector: Option<String>,
pub interval_secs: u64,
pub options: WatchOptions,
}
impl AddOptions {
pub fn with_interval(interval_secs: u64) -> Self {
Self {
interval_secs,
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
fn make_watch(interval_secs: u64, last_check_secs_ago: Option<i64>) -> Watch {
let last_check_at = last_check_secs_ago.map(|s| Utc::now() - chrono::Duration::seconds(s));
Watch {
id: "test0001".into(),
url: "https://example.com".into(),
selector: None,
interval_secs,
created_at: Utc::now(),
last_check_at,
last_change_at: None,
last_etag: None,
last_last_modified: None,
snapshots: vec![],
consecutive_errors: 0,
options: WatchOptions::default(),
}
}
#[test]
fn interval_due_logic_never_checked_is_always_due() {
let w = make_watch(3600, None);
assert!(w.is_due());
}
#[test]
fn interval_due_logic_checked_recently_is_not_due() {
let w = make_watch(3600, Some(30));
assert!(!w.is_due());
}
#[test]
fn interval_due_logic_checked_long_ago_is_due() {
let w = make_watch(3600, Some(7200));
assert!(w.is_due());
}
#[test]
fn muted_watch_never_due() {
let w = make_watch(0, None);
assert!(!w.is_due());
}
}