1mod entity_count;
84mod periodic_time;
85mod property_change;
86mod property_value_count;
87mod time;
88mod toggling_trigger;
89
90use std::marker::PhantomData;
91
92pub use entity_count::{EntityCountTrigger, EntityCountTriggerEvent};
93pub use periodic_time::{PeriodicTimeTrigger, PeriodicTimeTriggerEvent};
94pub use property_change::{PropertyChangeTrigger, PropertyChangeTriggerEvent};
95pub use property_value_count::{PropertyValueCountTrigger, PropertyValueCountTriggerEvent};
96pub use time::{TimeTrigger, TimeTriggerEvent};
97pub use toggling_trigger::{TogglingTrigger, TogglingTriggerCriteria};
98
99use crate::{Context, IxaEvent};
100
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub enum Direction {
104 Increasing,
105 Decreasing,
106}
107
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub enum TriggerMode {
111 Once,
112 Repeating,
113}
114
115pub trait TriggerCriterion: Sized + 'static {
118 type Observation: 'static;
121
122 fn install<F>(self, context: &mut Context, on_match: F)
124 where
125 F: Fn(&mut Context, Self::Observation) + 'static;
126
127 fn emit_with<Ev, F>(self, make_event: F) -> Trigger<Self, Ev, F>
129 where
130 Ev: IxaEvent,
131 F: Fn(Self::Observation) -> Ev + 'static,
132 {
133 Trigger {
134 criterion: self,
135 make_event,
136 _event: PhantomData,
137 }
138 }
139
140 fn emit_default<Ev>(self) -> Trigger<Self, Ev, impl Fn(Self::Observation) -> Ev>
142 where
143 Ev: IxaEvent + Default,
144 {
145 self.emit_with(|_| Ev::default())
146 }
147
148 fn emit_value<Ev>(self, event: Ev) -> Trigger<Self, Ev, impl Fn(Self::Observation) -> Ev>
150 where
151 Ev: IxaEvent,
152 {
153 self.emit_with(move |_| event)
154 }
155}
156
157pub trait TriggerSpec: Sized {
161 fn install_in_context(self, context: &mut Context);
162}
163
164pub struct Trigger<C, Ev, F> {
168 criterion: C,
169 make_event: F,
170 _event: PhantomData<fn() -> Ev>,
171}
172
173impl<C, Ev, F> TriggerSpec for Trigger<C, Ev, F>
174where
175 C: TriggerCriterion,
176 Ev: IxaEvent,
177 F: Fn(C::Observation) -> Ev + 'static,
178{
179 fn install_in_context(self, context: &mut Context) {
180 let make_event = self.make_event;
181 self.criterion
182 .install(context, move |context, observation| {
183 context.emit_event(make_event(observation));
184 });
185 }
186}
187
188pub trait ContextTriggersExt {
190 fn register_trigger<T: TriggerSpec>(&mut self, trigger: T);
191}
192
193impl ContextTriggersExt for Context {
194 fn register_trigger<T: TriggerSpec>(&mut self, trigger: T) {
195 trigger.install_in_context(self);
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 #![allow(dead_code)]
202
203 use std::cell::{Cell, RefCell};
204 use std::rc::Rc;
205
206 use super::*;
207 use crate::entity::EntityId;
208 use crate::{
209 define_entity, define_property, with, Context, ContextEntitiesExt, ExecutionPhase, IxaEvent,
210 };
211
212 define_entity!(Person);
213 define_entity!(Case);
214
215 define_property!(
216 enum InfectionStatus {
217 Susceptible,
218 Infectious,
219 Recovered,
220 },
221 Person,
222 default_const = InfectionStatus::Susceptible
223 );
224
225 define_property!(struct Alive(bool), Person, default_const = Alive(true));
226
227 define_property!(
228 enum CaseStatus {
229 Detected,
230 },
231 Case,
232 default_const = CaseStatus::Detected
233 );
234
235 #[derive(IxaEvent)]
236 struct InfectiousThresholdReached {
237 count: usize,
238 mode: TriggerMode,
239 }
240
241 #[derive(Default, IxaEvent)]
242 struct CaseThresholdReached;
243
244 #[derive(IxaEvent)]
245 struct FirstDeath {
246 person: EntityId<Person>,
247 mode: TriggerMode,
248 }
249
250 #[derive(IxaEvent)]
251 struct StopTimeReached {
252 phase: ExecutionPhase,
253 }
254
255 #[derive(IxaEvent)]
256 struct PeriodicTimeReached {
257 time: f64,
258 period: f64,
259 phase: ExecutionPhase,
260 }
261
262 #[test]
263 fn register_property_value_count_trigger() {
264 let mut context = Context::new();
265
266 context.register_trigger(
267 PropertyValueCountTrigger::<Person, InfectionStatus>::increases_to(
268 InfectionStatus::Infectious,
269 100,
270 )
271 .emit_with(|event| InfectiousThresholdReached {
272 count: event.count,
273 mode: event.mode,
274 }),
275 );
276
277 context.subscribe_to_event(|context, _event: InfectiousThresholdReached| {
278 context.shutdown();
279 });
280 }
281
282 #[test]
283 fn register_entity_count_trigger() {
284 let mut context = Context::new();
285
286 context.register_trigger(
287 EntityCountTrigger::<Case>::increases_to(10).emit_default::<CaseThresholdReached>(),
288 );
289 }
290
291 #[test]
292 fn register_property_change_trigger() {
293 let mut context = Context::new();
294
295 context.register_trigger(
296 PropertyChangeTrigger::<Person, Alive>::to(Alive(false)).emit_with(|event| {
297 FirstDeath {
298 person: event.entity_id,
299 mode: event.mode,
300 }
301 }),
302 );
303 }
304
305 #[test]
306 fn register_time_trigger() {
307 let mut context = Context::new();
308
309 context.register_trigger(
310 TimeTrigger::at(50.0).emit_with(|event| StopTimeReached { phase: event.phase }),
311 );
312 }
313
314 #[test]
315 fn register_periodic_time_trigger_default_phase() {
316 let mut context = Context::new();
317
318 context.register_trigger(PeriodicTimeTrigger::every(1.0).emit_with(|event| {
319 PeriodicTimeReached {
320 time: event.time,
321 period: event.period,
322 phase: event.phase,
323 }
324 }));
325 }
326
327 #[test]
328 fn register_constant_event_value() {
329 #[derive(IxaEvent)]
330 struct ShutdownRequested;
331
332 let mut context = Context::new();
333
334 context.register_trigger(
335 TimeTrigger::at_phase(50.0, ExecutionPhase::Last)
336 .emit_value::<ShutdownRequested>(ShutdownRequested),
337 );
338 }
339
340 #[test]
341 fn time_trigger_with_phase_sets_phase() {
342 let mut context = Context::new();
343 let observed_phase = Rc::new(Cell::new(None));
344 let observed_phase_clone = Rc::clone(&observed_phase);
345
346 context.register_trigger(
347 TimeTrigger::at(1.0)
348 .with_phase(ExecutionPhase::Last)
349 .emit_with(|event| StopTimeReached { phase: event.phase }),
350 );
351 context.subscribe_to_event(move |_context, event: StopTimeReached| {
352 observed_phase_clone.set(Some(event.phase));
353 });
354
355 context.execute();
356
357 assert_eq!(observed_phase.get(), Some(ExecutionPhase::Last));
358 }
359
360 #[test]
361 fn entity_count_trigger_emits_at_threshold() {
362 let mut context = Context::new();
363 let observed_count = Rc::new(Cell::new(0));
364 let observed_count_clone = Rc::clone(&observed_count);
365
366 #[derive(IxaEvent)]
367 struct CountReached {
368 count: usize,
369 }
370
371 context.register_trigger(
372 EntityCountTrigger::<Case>::increases_to(2)
373 .emit_with(|event| CountReached { count: event.count }),
374 );
375 context.subscribe_to_event(move |_context, event: CountReached| {
376 observed_count_clone.set(event.count);
377 });
378
379 context.add_entity(Case).unwrap();
380 context.add_entity(Case).unwrap();
381 context.execute();
382
383 assert_eq!(observed_count.get(), 2);
384 }
385
386 #[test]
387 fn periodic_time_trigger_emits_on_current_time_then_periodically() {
388 let mut context = Context::new();
389 let observed = Rc::new(RefCell::new(Vec::new()));
390 let observed_clone = Rc::clone(&observed);
391
392 context.register_trigger(PeriodicTimeTrigger::every(1.0).emit_with(|event| {
393 PeriodicTimeReached {
394 time: event.time,
395 period: event.period,
396 phase: event.phase,
397 }
398 }));
399 context.subscribe_to_event(move |_context, event: PeriodicTimeReached| {
400 assert_eq!(event.period, 1.0);
401 assert_eq!(event.phase, ExecutionPhase::Normal);
402 observed_clone.borrow_mut().push(event.time);
403 });
404
405 context.add_plan(2.0, |_| {});
406 context.execute();
407
408 assert_eq!(*observed.borrow(), vec![0.0, 1.0, 2.0]);
409 }
410
411 #[test]
412 fn periodic_time_trigger_start_with_delay() {
413 let mut context = Context::new();
414 let observed = Rc::new(RefCell::new(Vec::new()));
415 let observed_clone = Rc::clone(&observed);
416
417 context.register_trigger(
418 PeriodicTimeTrigger::every(1.0)
419 .start_with_delay(0.5)
420 .emit_with(|event| PeriodicTimeReached {
421 time: event.time,
422 period: event.period,
423 phase: event.phase,
424 }),
425 );
426 context.subscribe_to_event(move |_context, event: PeriodicTimeReached| {
427 observed_clone.borrow_mut().push(event.time);
428 });
429
430 context.add_plan(2.5, |_| {});
431 context.execute();
432
433 assert_eq!(*observed.borrow(), vec![0.5, 1.5, 2.5]);
434 }
435
436 #[test]
437 fn periodic_time_trigger_start_at() {
438 let mut context = Context::new();
439 let observed = Rc::new(RefCell::new(Vec::new()));
440 let observed_clone = Rc::clone(&observed);
441
442 context.register_trigger(PeriodicTimeTrigger::every(1.0).start_at(2.0).emit_with(
443 |event| PeriodicTimeReached {
444 time: event.time,
445 period: event.period,
446 phase: event.phase,
447 },
448 ));
449 context.subscribe_to_event(move |_context, event: PeriodicTimeReached| {
450 observed_clone.borrow_mut().push(event.time);
451 });
452
453 context.add_plan(4.0, |_| {});
454 context.execute();
455
456 assert_eq!(*observed.borrow(), vec![2.0, 3.0, 4.0]);
457 }
458
459 #[test]
460 fn periodic_time_trigger_uses_requested_phase() {
461 let mut context = Context::new();
462 let observed = Rc::new(RefCell::new(Vec::new()));
463
464 context.add_plan_with_phase(
465 1.0,
466 {
467 let observed = Rc::clone(&observed);
468 move |_| observed.borrow_mut().push("first")
469 },
470 ExecutionPhase::First,
471 );
472 context.add_plan_with_phase(
473 1.0,
474 {
475 let observed = Rc::clone(&observed);
476 move |_| observed.borrow_mut().push("normal")
477 },
478 ExecutionPhase::Normal,
479 );
480 context.add_plan_with_phase(
481 1.0,
482 {
483 let observed = Rc::clone(&observed);
484 move |_| observed.borrow_mut().push("last")
485 },
486 ExecutionPhase::Last,
487 );
488
489 PeriodicTimeTrigger::every_with_phase(1.0, ExecutionPhase::Last)
490 .start_at(1.0)
491 .install(&mut context, {
492 let observed = Rc::clone(&observed);
493 move |_context, event| {
494 assert_eq!(event.phase, ExecutionPhase::Last);
495 observed.borrow_mut().push("trigger");
496 }
497 });
498
499 context.execute();
500
501 assert_eq!(
502 *observed.borrow(),
503 vec!["first", "normal", "last", "trigger"]
504 );
505 }
506
507 #[test]
508 fn property_change_trigger_emits_matching_change() {
509 let mut context = Context::new();
510 let observed_person = Rc::new(Cell::new(None));
511 let observed_person_clone = Rc::clone(&observed_person);
512
513 #[derive(IxaEvent)]
514 struct BecameDead {
515 person: EntityId<Person>,
516 }
517
518 context.register_trigger(
519 PropertyChangeTrigger::<Person, Alive>::to(Alive(false)).emit_with(|event| {
520 BecameDead {
521 person: event.entity_id,
522 }
523 }),
524 );
525 context.subscribe_to_event(move |_context, event: BecameDead| {
526 observed_person_clone.set(Some(event.person));
527 });
528
529 let person = context.add_entity(Person).unwrap();
530 context.set_property(person, Alive(false));
531 context.execute();
532
533 assert_eq!(observed_person.get(), Some(person));
534 }
535
536 #[test]
537 fn property_change_trigger_defaults_to_repeating() {
538 let mut context = Context::new();
539 let observed_count = Rc::new(Cell::new(0));
540 let observed_count_clone = Rc::clone(&observed_count);
541
542 #[derive(IxaEvent)]
543 struct BecameDead {
544 mode: TriggerMode,
545 }
546
547 context.register_trigger(
548 PropertyChangeTrigger::<Person, Alive>::from_to(Alive(true), Alive(false))
549 .emit_with(|event| BecameDead { mode: event.mode }),
550 );
551 context.subscribe_to_event(move |_context, event: BecameDead| {
552 assert_eq!(event.mode, TriggerMode::Repeating);
553 observed_count_clone.set(observed_count_clone.get() + 1);
554 });
555
556 let person = context.add_entity(Person).unwrap();
557 context.set_property(person, Alive(false));
558 context.set_property(person, Alive(true));
559 context.set_property(person, Alive(false));
560 context.execute();
561
562 assert_eq!(observed_count.get(), 2);
563 }
564
565 #[test]
566 fn property_value_count_trigger_defaults_to_repeating() {
567 let mut context = Context::new();
568 let observed_count = Rc::new(Cell::new(0));
569 let observed_count_clone = Rc::clone(&observed_count);
570
571 #[derive(IxaEvent)]
572 struct InfectiousThresholdReached {
573 mode: TriggerMode,
574 }
575
576 context.register_trigger(
577 PropertyValueCountTrigger::<Person, InfectionStatus>::increases_to(
578 InfectionStatus::Infectious,
579 2,
580 )
581 .emit_with(|event| InfectiousThresholdReached { mode: event.mode }),
582 );
583 context.subscribe_to_event(move |_context, event: InfectiousThresholdReached| {
584 assert_eq!(event.mode, TriggerMode::Repeating);
585 observed_count_clone.set(observed_count_clone.get() + 1);
586 });
587
588 let first = context.add_entity(Person).unwrap();
589 let second = context.add_entity(Person).unwrap();
590 context.add_plan(0.1, move |context| {
591 context.set_property(first, InfectionStatus::Infectious);
592 });
593 context.add_plan(0.2, move |context| {
594 context.set_property(second, InfectionStatus::Infectious);
595 });
596 context.add_plan(0.3, move |context| {
597 context.set_property(second, InfectionStatus::Susceptible);
598 });
599 context.add_plan(0.4, move |context| {
600 context.set_property(second, InfectionStatus::Infectious);
601 });
602 context.execute();
603
604 assert_eq!(observed_count.get(), 2);
605 }
606
607 #[test]
608 fn property_value_count_trigger_changes_to_emits_in_either_direction() {
609 let mut context = Context::new();
610 let observed_directions = Rc::new(RefCell::new(Vec::new()));
611 let observed_directions_clone = Rc::clone(&observed_directions);
612
613 #[derive(IxaEvent)]
614 struct InfectiousThresholdReached {
615 direction_filter: Option<Direction>,
616 direction: Direction,
617 }
618
619 context.register_trigger(
620 PropertyValueCountTrigger::<Person, InfectionStatus>::changes_to(
621 InfectionStatus::Infectious,
622 2,
623 )
624 .repeating()
625 .emit_with(|event| InfectiousThresholdReached {
626 direction_filter: event.direction_filter,
627 direction: event.direction,
628 }),
629 );
630 context.subscribe_to_event(move |_context, event: InfectiousThresholdReached| {
631 assert_eq!(event.direction_filter, None);
632 observed_directions_clone.borrow_mut().push(event.direction);
633 });
634
635 let first = context.add_entity(Person).unwrap();
636 let second = context.add_entity(Person).unwrap();
637 let third = context.add_entity(Person).unwrap();
638 context.add_plan(0.1, move |context| {
639 context.set_property(first, InfectionStatus::Infectious);
640 });
641 context.add_plan(0.2, move |context| {
642 context.set_property(second, InfectionStatus::Infectious);
643 });
644 context.add_plan(0.3, move |context| {
645 context.set_property(third, InfectionStatus::Infectious);
646 });
647 context.add_plan(0.4, move |context| {
648 context.set_property(second, InfectionStatus::Susceptible);
649 });
650 context.execute();
651
652 assert_eq!(
653 *observed_directions.borrow(),
654 vec![Direction::Increasing, Direction::Decreasing]
655 );
656 }
657
658 #[test]
659 fn property_value_count_trigger_changes_to_ignores_no_op_writes() {
660 let mut context = Context::new();
661 let observed_count = Rc::new(Cell::new(0));
662 let observed_count_clone = Rc::clone(&observed_count);
663
664 #[derive(IxaEvent)]
665 struct InfectiousThresholdReached;
666
667 context.register_trigger(
668 PropertyValueCountTrigger::<Person, InfectionStatus>::changes_to(
669 InfectionStatus::Infectious,
670 1,
671 )
672 .repeating()
673 .emit_value::<InfectiousThresholdReached>(InfectiousThresholdReached),
674 );
675 context.subscribe_to_event(move |_context, _event: InfectiousThresholdReached| {
676 observed_count_clone.set(observed_count_clone.get() + 1);
677 });
678
679 let person = context.add_entity(Person).unwrap();
680 context.add_plan(0.1, move |context| {
681 context.set_property(person, InfectionStatus::Infectious);
682 });
683 context.add_plan(0.2, move |context| {
684 context.set_property(person, InfectionStatus::Infectious);
685 });
686 context.execute();
687
688 assert_eq!(observed_count.get(), 1);
689 }
690
691 #[test]
692 fn property_value_count_decreases_to_tracks_entities_created_with_tracked_value() {
693 let mut context = Context::new();
694 let observed = Rc::new(RefCell::new(Vec::new()));
695 let observed_clone = Rc::clone(&observed);
696
697 #[derive(IxaEvent)]
698 struct InfectiousThresholdReached {
699 count: usize,
700 direction: Direction,
701 }
702
703 context.register_trigger(
704 PropertyValueCountTrigger::<Person, InfectionStatus>::decreases_to(
705 InfectionStatus::Infectious,
706 1,
707 )
708 .repeating()
709 .emit_with(|event| InfectiousThresholdReached {
710 count: event.count,
711 direction: event.direction,
712 }),
713 );
714 context.subscribe_to_event(move |_context, event: InfectiousThresholdReached| {
715 observed_clone
716 .borrow_mut()
717 .push((event.count, event.direction));
718 });
719
720 let first = context
721 .add_entity(with!(Person, InfectionStatus::Infectious))
722 .unwrap();
723 let _second = context
724 .add_entity(with!(Person, InfectionStatus::Infectious))
725 .unwrap();
726 context.add_plan(0.1, move |context| {
727 context.set_property(first, InfectionStatus::Susceptible);
728 });
729 context.execute();
730
731 assert_eq!(*observed.borrow(), vec![(1, Direction::Decreasing)]);
732 }
733
734 #[test]
735 #[should_panic(expected = "period must be greater than 0")]
736 fn periodic_time_trigger_zero_period_panics() {
737 let _ = PeriodicTimeTrigger::every(0.0);
738 }
739
740 #[test]
741 #[should_panic(expected = "period must be greater than 0")]
742 fn periodic_time_trigger_negative_period_panics() {
743 let _ = PeriodicTimeTrigger::every(-1.0);
744 }
745
746 #[test]
747 #[should_panic(expected = "period must be greater than 0")]
748 fn periodic_time_trigger_nan_period_panics() {
749 let _ = PeriodicTimeTrigger::every(f64::NAN);
750 }
751
752 #[test]
753 #[should_panic(expected = "period must be greater than 0")]
754 fn periodic_time_trigger_infinite_period_panics() {
755 let _ = PeriodicTimeTrigger::every(f64::INFINITY);
756 }
757
758 #[test]
759 #[should_panic(expected = "period must be greater than 0")]
760 fn periodic_time_trigger_every_with_phase_validates_period() {
761 let _ = PeriodicTimeTrigger::every_with_phase(0.0, ExecutionPhase::Last);
762 }
763
764 #[test]
765 #[should_panic(expected = "delay must be greater than or equal to 0")]
766 fn periodic_time_trigger_negative_delay_panics() {
767 let _ = PeriodicTimeTrigger::every(1.0).start_with_delay(-1.0);
768 }
769
770 #[test]
771 #[should_panic(expected = "delay must be greater than or equal to 0")]
772 fn periodic_time_trigger_nan_delay_panics() {
773 let _ = PeriodicTimeTrigger::every(1.0).start_with_delay(f64::NAN);
774 }
775
776 #[test]
777 #[should_panic(expected = "delay must be greater than or equal to 0")]
778 fn periodic_time_trigger_infinite_delay_panics() {
779 let _ = PeriodicTimeTrigger::every(1.0).start_with_delay(f64::INFINITY);
780 }
781
782 #[test]
783 #[should_panic(expected = "cannot be NaN")]
784 fn periodic_time_trigger_nan_start_time_panics() {
785 let _ = PeriodicTimeTrigger::every(1.0).start_at(f64::NAN);
786 }
787
788 #[test]
789 #[should_panic(expected = "cannot be infinite")]
790 fn periodic_time_trigger_infinite_start_time_panics() {
791 let _ = PeriodicTimeTrigger::every(1.0).start_at(f64::INFINITY);
792 }
793
794 #[test]
795 #[should_panic(expected = "cannot be less than the current time")]
796 fn periodic_time_trigger_start_at_past_panics() {
797 let mut context = Context::new();
798 context.add_plan(1.0, |_| {});
799 context.execute();
800
801 context.register_trigger(
802 PeriodicTimeTrigger::every(1.0)
803 .start_at(0.5)
804 .emit_value::<CaseThresholdReached>(CaseThresholdReached),
805 );
806 }
807}