use crate::nfc_uid::NfcUid;
use chrono::{DateTime, Utc};
use serde::Deserialize;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::time::Duration;
#[derive(Debug, Copy, Clone, Default, Deserialize)]
pub enum TriggerOccurrence {
#[default]
Once,
Times(usize),
Duration(Duration),
}
#[derive(Debug, Copy, Clone, Deserialize)]
pub struct TriggerSpec<T> {
#[serde(default)]
pub global: bool,
#[serde(default)]
pub occurrence: TriggerOccurrence,
pub trigger: T,
}
#[derive(Debug)]
struct ActiveTrigger<T> {
spec: TriggerSpec<T>,
triggered_at: DateTime<Utc>,
usages: usize,
}
impl<T> ActiveTrigger<T> {
fn new(spec: TriggerSpec<T>) -> Self {
Self {
spec,
triggered_at: Utc::now(),
usages: 0,
}
}
}
impl<T: PartialEq> ActiveTrigger<T> {
fn check(&mut self, trigger: T, reference_time: DateTime<Utc>) -> (bool, bool) {
if trigger != self.spec.trigger {
return (false, true);
}
match self.spec.occurrence {
TriggerOccurrence::Once => (true, false),
TriggerOccurrence::Times(times) => {
let active = self.usages < times;
self.usages += 1;
(active, self.usages < times)
}
TriggerOccurrence::Duration(duration) => {
let still_active = self.triggered_at + duration >= reference_time;
(still_active, still_active)
}
}
}
}
#[derive(Debug)]
pub struct TriggerRegistry<T> {
trigger_specs: HashMap<NfcUid, TriggerSpec<T>>,
active_local_triggers: HashMap<String, ActiveTrigger<T>>,
active_global_trigger: Option<ActiveTrigger<T>>,
}
impl<T> From<HashMap<NfcUid, TriggerSpec<T>>> for TriggerRegistry<T> {
fn from(specs: HashMap<NfcUid, TriggerSpec<T>>) -> Self {
Self::new(specs)
}
}
impl<T> TriggerRegistry<T> {
pub fn new(specs: HashMap<NfcUid, TriggerSpec<T>>) -> Self {
Self {
trigger_specs: specs,
active_local_triggers: HashMap::new(),
active_global_trigger: None,
}
}
}
impl<T: PartialEq> TriggerRegistry<T> {
pub(crate) fn check_active_trigger(
&mut self,
trigger: T,
client_id: &str,
reference_time: DateTime<Utc>,
) -> bool {
if let Entry::Occupied(mut active_trigger) =
self.active_local_triggers.entry(client_id.to_string())
{
let (active, retain) = active_trigger.get_mut().check(trigger, reference_time);
if !retain {
active_trigger.remove();
}
return active;
}
self.active_global_trigger
.take()
.is_some_and(|mut active_trigger| {
let (active, retain) = active_trigger.check(trigger, reference_time);
if retain {
self.active_global_trigger = Some(active_trigger);
}
active
})
}
}
impl<T: Copy> TriggerRegistry<T> {
pub(crate) fn try_activate_trigger(&mut self, nfc_uid: NfcUid, client_id: &str) -> bool {
let Some(spec) = self.trigger_specs.get(&nfc_uid) else {
return false;
};
let active_trigger = ActiveTrigger::new(*spec);
if spec.global {
self.active_global_trigger = Some(active_trigger);
} else {
self.active_local_triggers
.insert(client_id.to_string(), active_trigger);
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use std::time::Duration;
#[test]
fn activate_and_check_once_trigger() {
let uid = NfcUid::default();
let mut registry: TriggerRegistry<u8> = HashMap::from_iter([(
uid,
TriggerSpec {
global: false,
occurrence: TriggerOccurrence::Once,
trigger: 42,
},
)])
.into();
assert!(registry.try_activate_trigger(uid, "client1"));
let now = Utc::now();
assert!(registry.check_active_trigger(42, "client1", now));
assert!(!registry.check_active_trigger(42, "client1", now));
}
#[test]
fn activate_and_check_times_trigger() {
let uid = NfcUid::default();
let mut registry: TriggerRegistry<u8> = HashMap::from_iter([(
uid,
TriggerSpec {
global: false,
occurrence: TriggerOccurrence::Times(2),
trigger: 42,
},
)])
.into();
assert!(registry.try_activate_trigger(uid, "client2"));
let now = Utc::now();
assert!(registry.check_active_trigger(42, "client2", now));
assert!(registry.check_active_trigger(42, "client2", now));
assert!(!registry.check_active_trigger(42, "client2", now));
}
#[test]
fn activate_and_check_duration_trigger() {
let uid = NfcUid::default();
let mut registry: TriggerRegistry<u8> = HashMap::from_iter([(
uid,
TriggerSpec {
global: false,
occurrence: TriggerOccurrence::Duration(Duration::from_secs(50)),
trigger: 42,
},
)])
.into();
assert!(registry.try_activate_trigger(uid, "client3"));
let now = Utc::now();
assert!(registry.check_active_trigger(42, "client3", now));
let later = now + Duration::from_secs(30);
assert!(registry.check_active_trigger(42, "client3", later));
let expired = now + Duration::from_secs(70);
assert!(!registry.check_active_trigger(42, "client3", expired));
}
#[test]
fn global_trigger_works_from_any_client() {
let uid = NfcUid::default();
let mut registry: TriggerRegistry<u8> = HashMap::from_iter([(
uid,
TriggerSpec {
global: true,
occurrence: TriggerOccurrence::Once,
trigger: 42,
},
)])
.into();
assert!(registry.try_activate_trigger(uid, "any_client"));
let now = Utc::now();
assert!(registry.check_active_trigger(42, "client1", now));
assert!(!registry.check_active_trigger(42, "client2", now));
}
}