use crate::core::{DeadlineSpec, Digest, Timestamp};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CalendarError {
#[error("this calendar does not implement deadline kind '{0}'")]
UnknownKind(String),
#[error("malformed parameters for '{kind}': {detail}")]
BadParams { kind: String, detail: String },
#[error("'{kind}' resolved outside the representable range")]
OutOfRange { kind: String },
}
pub trait Calendar: Send + Sync + std::fmt::Debug {
fn resolve(&self, from: Timestamp, spec: &DeadlineSpec) -> Result<Timestamp, CalendarError>;
fn digest(&self) -> Digest;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct WallClock;
impl WallClock {
fn count(spec: &DeadlineSpec) -> Result<i64, CalendarError> {
spec.params
.get("n")
.and_then(serde_json::Value::as_i64)
.ok_or_else(|| CalendarError::BadParams {
kind: spec.kind.clone(),
detail: "expected an integer field `n`".into(),
})
}
}
impl Calendar for WallClock {
fn resolve(&self, from: Timestamp, spec: &DeadlineSpec) -> Result<Timestamp, CalendarError> {
let n = Self::count(spec)?;
let delta = match spec.kind.as_str() {
"hours" => time::Duration::hours(n),
"days" => time::Duration::days(n),
"minutes" => time::Duration::minutes(n),
other => return Err(CalendarError::UnknownKind(other.to_owned())),
};
from.checked_add(delta).ok_or(CalendarError::OutOfRange {
kind: spec.kind.clone(),
})
}
fn digest(&self) -> Digest {
Digest::of(b"agentplane.calendar.wallclock.v1")
}
}
#[cfg(test)]
mod tests {
use super::*;
use time::macros::datetime;
#[test]
fn resolves_hours_and_days() {
let base = datetime!(2026-07-30 09:00:00 UTC);
assert_eq!(
WallClock.resolve(base, &DeadlineSpec::hours(24)).unwrap(),
datetime!(2026-07-31 09:00:00 UTC)
);
assert_eq!(
WallClock.resolve(base, &DeadlineSpec::days(5)).unwrap(),
datetime!(2026-08-04 09:00:00 UTC)
);
}
#[test]
fn resolution_is_pure() {
let base = datetime!(2026-07-30 09:00:00 UTC);
let spec = DeadlineSpec::hours(72);
assert_eq!(
WallClock.resolve(base, &spec).unwrap(),
WallClock.resolve(base, &spec).unwrap()
);
}
#[test]
fn unknown_rules_are_refused_not_approximated() {
let err = WallClock
.resolve(
datetime!(2026-07-30 09:00:00 UTC),
&DeadlineSpec::new("working-days", serde_json::json!({ "n": 5 })),
)
.unwrap_err();
assert!(matches!(err, CalendarError::UnknownKind(_)));
}
#[test]
fn malformed_parameters_are_refused() {
let err = WallClock
.resolve(
datetime!(2026-07-30 09:00:00 UTC),
&DeadlineSpec::new("hours", serde_json::json!({ "wrong": 1 })),
)
.unwrap_err();
assert!(matches!(err, CalendarError::BadParams { .. }));
}
#[test]
fn digest_is_stable_for_a_ruleset() {
assert_eq!(WallClock.digest(), WallClock.digest());
}
}