use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::error::{KeepsakeError, Result};
use crate::model::FulfillmentSnapshot;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ExpiryPolicy {
ManualOnly,
At {
timestamp: DateTime<Utc>,
},
WhenFulfilled {
policy: FulfillmentPolicy,
},
}
impl ExpiryPolicy {
#[must_use]
pub const fn timed_expiry(&self) -> Option<DateTime<Utc>> {
match self {
Self::ManualOnly | Self::WhenFulfilled { .. } => None,
Self::At { timestamp } => Some(*timestamp),
}
}
pub const fn validate(&self) -> Result<()> {
match self {
Self::ManualOnly | Self::At { .. } => Ok(()),
Self::WhenFulfilled { policy } => policy.validate(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum FulfillmentPolicy {
CounterAtLeast {
key: String,
threshold: i64,
},
ChecklistComplete {
list_key: String,
},
}
impl FulfillmentPolicy {
pub const fn validate(&self) -> Result<()> {
match self {
Self::CounterAtLeast { threshold, .. } if *threshold <= 0 => {
Err(KeepsakeError::InvalidFulfillmentThreshold)
}
Self::CounterAtLeast { .. } | Self::ChecklistComplete { .. } => Ok(()),
}
}
#[must_use]
pub fn is_fulfilled(&self, snapshot: &FulfillmentSnapshot) -> bool {
match self {
Self::CounterAtLeast { key, threshold } => snapshot
.counters
.get(key)
.is_some_and(|value| value >= threshold),
Self::ChecklistComplete { list_key } => {
let mut matched = false;
for (key, complete) in &snapshot.checklist {
if key.starts_with(list_key) {
matched = true;
if !complete {
return false;
}
}
}
matched
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counter_threshold_must_be_positive() {
let policy = FulfillmentPolicy::CounterAtLeast {
key: "steps".to_owned(),
threshold: 0,
};
assert_eq!(
policy.validate(),
Err(KeepsakeError::InvalidFulfillmentThreshold)
);
}
#[test]
fn counter_policy_uses_the_named_counter_and_inclusive_threshold() {
let policy = FulfillmentPolicy::CounterAtLeast {
key: "steps".to_owned(),
threshold: 3,
};
assert!(!policy.is_fulfilled(&FulfillmentSnapshot::empty()));
assert!(
!policy.is_fulfilled(
&FulfillmentSnapshot::empty()
.with_counter("other", 10)
.with_counter("steps", 2)
)
);
assert!(policy.is_fulfilled(&FulfillmentSnapshot::empty().with_counter("steps", 3)));
}
#[test]
fn checklist_requires_at_least_one_matching_complete_item() {
let policy = FulfillmentPolicy::ChecklistComplete {
list_key: "onboarding.".to_owned(),
};
assert!(!policy.is_fulfilled(&FulfillmentSnapshot::empty()));
assert!(!policy.is_fulfilled(&FulfillmentSnapshot::empty().with_check("other.done", true)));
assert!(
!policy.is_fulfilled(
&FulfillmentSnapshot::empty()
.with_check("onboarding.profile", true)
.with_check("onboarding.terms", false)
)
);
assert!(
policy.is_fulfilled(
&FulfillmentSnapshot::empty()
.with_check("onboarding.profile", true)
.with_check("onboarding.terms", true)
)
);
}
}