1use crate::nfc_uid::NfcUid;
29use chrono::{DateTime, Utc};
30use serde::Deserialize;
31use std::collections::HashMap;
32use std::collections::hash_map::Entry;
33use std::time::Duration;
34
35#[derive(Debug, Copy, Clone, Default, Deserialize)]
37pub enum TriggerOccurrence {
38 #[default]
40 Once,
41 Times(usize),
43 Duration(Duration),
45}
46
47#[derive(Debug, Copy, Clone, Deserialize)]
49pub struct TriggerSpec<T> {
50 #[serde(default)]
52 pub global: bool,
53 #[serde(default)]
55 pub occurrence: TriggerOccurrence,
56 pub trigger: T,
58}
59
60#[derive(Debug)]
62struct ActiveTrigger<T> {
63 spec: TriggerSpec<T>,
65 triggered_at: DateTime<Utc>,
67 usages: usize,
69}
70
71impl<T> ActiveTrigger<T> {
72 fn new(spec: TriggerSpec<T>) -> Self {
76 Self {
77 spec,
78 triggered_at: Utc::now(),
79 usages: 0,
80 }
81 }
82}
83
84impl<T: PartialEq> ActiveTrigger<T> {
85 fn check(&mut self, trigger: T, reference_time: DateTime<Utc>) -> (bool, bool) {
88 if trigger != self.spec.trigger {
89 return (false, true);
91 }
92
93 match self.spec.occurrence {
94 TriggerOccurrence::Once => (true, false),
95 TriggerOccurrence::Times(times) => {
96 let active = self.usages < times;
97 self.usages += 1;
98 (active, self.usages < times)
99 }
100 TriggerOccurrence::Duration(duration) => {
101 let still_active = self.triggered_at + duration >= reference_time;
102 (still_active, still_active)
103 }
104 }
105 }
106}
107
108#[derive(Debug)]
110pub struct TriggerRegistry<T> {
111 trigger_specs: HashMap<NfcUid, TriggerSpec<T>>,
112 active_local_triggers: HashMap<String, ActiveTrigger<T>>,
113 active_global_trigger: Option<ActiveTrigger<T>>,
114}
115
116impl<T> From<HashMap<NfcUid, TriggerSpec<T>>> for TriggerRegistry<T> {
117 fn from(specs: HashMap<NfcUid, TriggerSpec<T>>) -> Self {
118 Self::new(specs)
119 }
120}
121
122impl<T> TriggerRegistry<T> {
123 pub fn new(specs: HashMap<NfcUid, TriggerSpec<T>>) -> Self {
125 Self {
126 trigger_specs: specs,
127 active_local_triggers: HashMap::new(),
128 active_global_trigger: None,
129 }
130 }
131}
132
133impl<T: PartialEq> TriggerRegistry<T> {
134 pub(crate) fn check_active_trigger(
138 &mut self,
139 trigger: T,
140 client_id: &str,
141 reference_time: DateTime<Utc>,
142 ) -> bool {
143 if let Entry::Occupied(mut active_trigger) =
144 self.active_local_triggers.entry(client_id.to_string())
145 {
146 let (active, retain) = active_trigger.get_mut().check(trigger, reference_time);
147
148 if !retain {
149 active_trigger.remove();
150 }
151
152 return active;
153 }
154
155 self.active_global_trigger
156 .take()
157 .is_some_and(|mut active_trigger| {
158 let (active, retain) = active_trigger.check(trigger, reference_time);
159
160 if retain {
161 self.active_global_trigger = Some(active_trigger);
162 }
163
164 active
165 })
166 }
167}
168
169impl<T: Copy> TriggerRegistry<T> {
170 pub(crate) fn try_activate_trigger(&mut self, nfc_uid: NfcUid, client_id: &str) -> bool {
174 let Some(spec) = self.trigger_specs.get(&nfc_uid) else {
175 return false;
176 };
177
178 let active_trigger = ActiveTrigger::new(*spec);
179
180 if spec.global {
181 self.active_global_trigger = Some(active_trigger);
182 } else {
183 self.active_local_triggers
184 .insert(client_id.to_string(), active_trigger);
185 }
186
187 true
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use chrono::Utc;
195 use std::time::Duration;
196
197 #[test]
198 fn activate_and_check_once_trigger() {
199 let uid = NfcUid::default();
200 let mut registry: TriggerRegistry<u8> = HashMap::from_iter([(
201 uid,
202 TriggerSpec {
203 global: false,
204 occurrence: TriggerOccurrence::Once,
205 trigger: 42,
206 },
207 )])
208 .into();
209
210 assert!(registry.try_activate_trigger(uid, "client1"));
211
212 let now = Utc::now();
213 assert!(registry.check_active_trigger(42, "client1", now));
214 assert!(!registry.check_active_trigger(42, "client1", now));
215 }
216
217 #[test]
218 fn activate_and_check_times_trigger() {
219 let uid = NfcUid::default();
220 let mut registry: TriggerRegistry<u8> = HashMap::from_iter([(
221 uid,
222 TriggerSpec {
223 global: false,
224 occurrence: TriggerOccurrence::Times(2),
225 trigger: 42,
226 },
227 )])
228 .into();
229
230 assert!(registry.try_activate_trigger(uid, "client2"));
231
232 let now = Utc::now();
233 assert!(registry.check_active_trigger(42, "client2", now));
234 assert!(registry.check_active_trigger(42, "client2", now));
235 assert!(!registry.check_active_trigger(42, "client2", now));
236 }
237
238 #[test]
239 fn activate_and_check_duration_trigger() {
240 let uid = NfcUid::default();
241 let mut registry: TriggerRegistry<u8> = HashMap::from_iter([(
242 uid,
243 TriggerSpec {
244 global: false,
245 occurrence: TriggerOccurrence::Duration(Duration::from_secs(50)),
246 trigger: 42,
247 },
248 )])
249 .into();
250
251 assert!(registry.try_activate_trigger(uid, "client3"));
252
253 let now = Utc::now();
254 assert!(registry.check_active_trigger(42, "client3", now));
255
256 let later = now + Duration::from_secs(30);
257 assert!(registry.check_active_trigger(42, "client3", later));
258
259 let expired = now + Duration::from_secs(70);
260 assert!(!registry.check_active_trigger(42, "client3", expired));
261 }
262
263 #[test]
264 fn global_trigger_works_from_any_client() {
265 let uid = NfcUid::default();
266 let mut registry: TriggerRegistry<u8> = HashMap::from_iter([(
267 uid,
268 TriggerSpec {
269 global: true,
270 occurrence: TriggerOccurrence::Once,
271 trigger: 42,
272 },
273 )])
274 .into();
275
276 assert!(registry.try_activate_trigger(uid, "any_client"));
277
278 let now = Utc::now();
279 assert!(registry.check_active_trigger(42, "client1", now));
280 assert!(!registry.check_active_trigger(42, "client2", now));
281 }
282}