use anyhow::{Result, bail};
use super::NotificationRequest;
use crate::notification_backend::replay::{EndAt, StartAt};
impl NotificationRequest {
pub fn has_end_point(&self) -> bool {
self.to_id.is_some() || self.to_date.is_some()
}
pub fn reject_end_point(&self, endpoint: &str) -> Result<()> {
if self.has_end_point() {
bail!("to_id and to_date are only supported for the replay endpoint, not {endpoint}");
}
Ok(())
}
pub fn validate_end_at(&self, start_at: StartAt) -> Result<EndAt> {
let to_id = Self::parse_sequence_field("to_id", self.to_id.as_deref())?;
let to_date = Self::parse_date_field("to_date", self.to_date.as_deref())?;
match (to_id, to_date) {
(Some(_), Some(_)) => bail!(
"Cannot specify both to_id and to_date. Please provide only one replay end \
parameter. Use to_id for a sequence-based end or to_date for a time-based end."
),
(Some(id), None) => {
if let StartAt::Sequence(from_id) = start_at
&& id < from_id
{
bail!("to_id ({id}) must not be lower than from_id ({from_id})");
}
Ok(EndAt::Sequence(id))
}
(None, Some(date)) => {
if let StartAt::Date(from_date) = start_at
&& date < from_date
{
bail!(
"to_date ({}) must not be earlier than from_date ({})",
date.to_rfc3339(),
from_date.to_rfc3339()
);
}
Ok(EndAt::Date(date))
}
(None, None) => Ok(EndAt::Latest),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{DateTime, Utc};
use std::collections::HashMap;
fn request(to_id: Option<&str>, to_date: Option<&str>) -> NotificationRequest {
NotificationRequest {
event_type: "test_polygon".to_string(),
identifier: HashMap::new(),
from_id: None,
from_date: None,
to_id: to_id.map(str::to_string),
to_date: to_date.map(str::to_string),
payload: None,
}
}
fn date(text: &str) -> DateTime<Utc> {
text.parse().unwrap()
}
#[test]
fn no_end_point_ends_at_the_latest_message() {
let end = request(None, None).validate_end_at(StartAt::Sequence(1));
assert_eq!(end.unwrap(), EndAt::Latest);
assert!(!request(None, None).has_end_point());
}
#[test]
fn to_id_is_an_inclusive_sequence_not_below_from_id() {
let at_start = request(Some("100"), None).validate_end_at(StartAt::Sequence(100));
assert_eq!(at_start.unwrap(), EndAt::Sequence(100));
let later = request(Some("250"), None).validate_end_at(StartAt::Sequence(100));
assert_eq!(later.unwrap(), EndAt::Sequence(250));
let error = request(Some("99"), None)
.validate_end_at(StartAt::Sequence(100))
.unwrap_err();
assert_eq!(
error.to_string(),
"to_id (99) must not be lower than from_id (100)"
);
}
#[test]
fn to_date_accepts_the_from_date_formats_and_is_not_before_from_date() {
for text in ["2025-06-09T13:15:00Z", "2025-06-09 13:15:00", "1749474900"] {
let end = request(None, Some(text)).validate_end_at(StartAt::Sequence(1));
assert_eq!(
end.unwrap(),
EndAt::Date(date("2025-06-09T13:15:00Z")),
"{text}"
);
}
let error = request(None, Some("2025-06-09T13:14:59Z"))
.validate_end_at(StartAt::Date(date("2025-06-09T13:15:00Z")))
.unwrap_err();
assert!(
error
.to_string()
.starts_with("to_date (2025-06-09T13:14:59+00:00) must not be earlier"),
"{error}"
);
}
#[test]
fn an_end_of_the_other_kind_is_not_compared_with_the_start() {
let end = request(None, Some("2000-01-01T00:00:00Z")).validate_end_at(StartAt::Sequence(5));
assert_eq!(end.unwrap(), EndAt::Date(date("2000-01-01T00:00:00Z")));
let end =
request(Some("0"), None).validate_end_at(StartAt::Date(date("2030-01-01T00:00:00Z")));
assert_eq!(end.unwrap(), EndAt::Sequence(0));
}
#[test]
fn invalid_end_points_are_rejected() {
let both = request(Some("5"), Some("2025-06-09T13:15:00Z"));
assert!(both.has_end_point());
let error = both.validate_end_at(StartAt::Sequence(1)).unwrap_err();
assert!(
error
.to_string()
.starts_with("Cannot specify both to_id and to_date")
);
let cases = [
(Some(""), None, "to_id cannot be empty"),
(Some("-3"), None, "to_id must be a valid positive integer"),
(None, Some(" "), "to_date cannot be empty"),
(
None,
Some("yesterday"),
"to_date must be a valid datetime/timestamp",
),
];
for (to_id, to_date, expected) in cases {
let error = request(to_id, to_date)
.validate_end_at(StartAt::Sequence(1))
.unwrap_err();
assert!(error.to_string().starts_with(expected), "{error}");
}
}
}