Skip to main content

ixa/triggers/
toggling_trigger.rs

1use std::cell::Cell;
2use std::marker::PhantomData;
3use std::rc::Rc;
4
5use super::{TriggerCriterion, TriggerMode, TriggerSpec};
6use crate::{Context, IxaEvent};
7
8/// A pair of activation and deactivation criteria that can be bound to emitted events.
9///
10/// [`TogglingTriggerCriteria`] composes two trigger criteria: one activation criterion and one
11/// deactivation criterion. The trigger starts inactive by default. When the activation criterion
12/// matches while inactive, the trigger becomes active and emits the activation event. Later
13/// activation matches are ignored while the trigger remains active. When the deactivation criterion
14/// matches while active, the trigger becomes inactive and emits the deactivation event. Later
15/// deactivation matches are ignored while the trigger remains inactive.
16///
17/// This is useful for thermostat-style hysteresis. For example, a model can activate an
18/// intervention when a property-value count reaches a lower threshold and deactivate it when the
19/// same count reaches an upper threshold. The thresholds themselves are ordinary criteria; the
20/// toggling trigger only gates those criteria by its current active/inactive state.
21///
22/// ## Construction
23///
24/// ```rust,ignore
25/// TogglingTriggerCriteria::new(activation_criterion, deactivation_criterion)
26/// TogglingTriggerCriteria::new(activation_criterion, deactivation_criterion).initially_active()
27/// TogglingTriggerCriteria::new(activation_criterion, deactivation_criterion).initially_inactive()
28/// TogglingTriggerCriteria::new(activation_criterion, deactivation_criterion).once()
29/// TogglingTriggerCriteria::new(activation_criterion, deactivation_criterion).repeating()
30/// TogglingTriggerCriteria::new(activation_criterion, deactivation_criterion)
31///     .emit_with(make_active_event, make_inactive_event)
32/// TogglingTriggerCriteria::new(activation_criterion, deactivation_criterion)
33///     .emit_values(active_event, inactive_event)
34/// TogglingTriggerCriteria::new(activation_criterion, deactivation_criterion)
35///     .emit_defaults::<ActiveEv, InactiveEv>()
36/// ```
37///
38/// ```rust,ignore
39/// TogglingTrigger::new(
40///     activation_criterion,
41///     make_active_event,
42///     deactivation_criterion,
43///     make_inactive_event,
44/// )
45/// TogglingTrigger::new(
46///     activation_criterion,
47///     make_active_event,
48///     deactivation_criterion,
49///     make_inactive_event,
50/// ).initially_active()
51/// TogglingTrigger::new(
52///     activation_criterion,
53///     make_active_event,
54///     deactivation_criterion,
55///     make_inactive_event,
56/// ).initially_inactive()
57/// TogglingTrigger::new(
58///     activation_criterion,
59///     make_active_event,
60///     deactivation_criterion,
61///     make_inactive_event,
62/// ).once()
63/// TogglingTrigger::new(
64///     activation_criterion,
65///     make_active_event,
66///     deactivation_criterion,
67///     make_inactive_event,
68/// ).repeating()
69/// ```
70///
71/// ## Observation
72///
73/// `TogglingTrigger` does not define its own observation struct. The activation event constructor
74/// receives the activation criterion's observation, and the deactivation event constructor receives
75/// the deactivation criterion's observation:
76///
77/// ```rust,ignore
78/// make_active_event: impl Fn(AC::Observation) -> ActiveEv
79/// make_inactive_event: impl Fn(DC::Observation) -> InactiveEv
80/// ```
81///
82/// ## Semantics
83///
84/// - If the trigger is inactive and the activation criterion matches, a single activation event is
85///   emitted and the trigger's internal state is changed to active.
86/// - If the trigger is inactive and the deactivation criterion matches, there is no effect.
87/// - If the trigger is active and the activation criterion matches, there is no effect.
88/// - If the trigger is active and the deactivation criterion matches, a single deactivation event
89///   is emitted and the trigger's internal state is changed to inactive.
90///
91/// ### Repeating or once
92///
93/// The "mode" of the completed [`TogglingTrigger`] controls how long the active/inactive state
94/// machine remains enabled. A toggling trigger defaults to
95/// [`TriggerMode::Repeating`](super::TriggerMode::Repeating), whether it is constructed through
96/// [`TogglingTriggerCriteria::emit_with`] or directly with [`TogglingTrigger::new`]. In repeating
97/// mode, it can activate, deactivate, and activate again for as long as its component criteria
98/// continue to match.
99///
100/// Calling [`TogglingTriggerCriteria::once`] or [`TogglingTrigger::once`] sets the completed
101/// toggling trigger to [`TriggerMode::Once`]. For a toggling trigger, "once" means one active
102/// period, _not_ one raw criterion match. If the trigger starts inactive, it can emit one activation
103/// event and then one deactivation event. After that deactivation event, the toggling trigger is
104/// permanently disabled and ignores all later criterion matches. If the trigger starts active with
105/// [`TogglingTriggerCriteria::initially_active`] or [`TogglingTrigger::initially_active`], the one
106/// active period is already in progress; the first accepted deactivation emits the deactivation
107/// event and then disables the trigger. (Matches of the underlying criterion that are ignored
108/// because they occur in the wrong active/inactive state do not by themselves disable a `once`
109/// toggling trigger.)
110///
111/// The mode of the completed `TogglingTrigger` should not be confused with the mode of each component
112/// criterion, which controls how often that individual criterion reports matches to the toggling
113/// trigger. In fact, component criteria should almost always be repeating even when the
114/// `TogglingTrigger` itself is configured with [`TogglingTriggerCriteria::once`] or
115/// [`TogglingTrigger::once`]. If an underlying criterion uses
116/// [`TriggerMode::Once`](super::TriggerMode::Once), that criterion can be consumed by a match that
117/// the toggling trigger ignores because it occurred in the wrong state. For example, a once-only
118/// activation criterion can match while the toggling trigger is already active; the toggling trigger
119/// will correctly ignore that activation match, but the activation criterion may never report
120/// another match.
121///
122/// [`TogglingTrigger::new`] is also available for all-at-once construction of a complete
123/// `TogglingTrigger`.
124///
125/// ## Example
126///
127/// ```rust
128/// use ixa::{Context, ContextEntitiesExt, define_entity, define_property, IxaEvent};
129/// use ixa::triggers::{
130///     ContextTriggersExt, PropertyValueCountTrigger, TogglingTriggerCriteria,
131/// };
132///
133/// define_entity!(Person);
134/// define_property!(
135///     enum InfectionStatus {
136///         Susceptible,
137///         Infectious,
138///     },
139///     Person,
140///     default_const = InfectionStatus::Susceptible
141/// );
142///
143/// #[derive(IxaEvent)]
144/// struct InterventionActivated {
145///     count: usize,
146/// }
147///
148/// #[derive(IxaEvent)]
149/// struct InterventionDeactivated {
150///     count: usize,
151/// }
152///
153/// let mut context = Context::new();
154///
155/// context.register_trigger(TogglingTriggerCriteria::new(
156///     PropertyValueCountTrigger::changes_to(
157///         InfectionStatus::Infectious,
158///         10,
159///     ),
160///     PropertyValueCountTrigger::changes_to(
161///         InfectionStatus::Infectious,
162///         25,
163///     ),
164/// ).emit_with(
165///     |event| InterventionActivated { count: event.count },
166///     |event| InterventionDeactivated { count: event.count },
167/// ));
168///
169/// context.subscribe_to_event(|_context, _event: InterventionActivated| {
170///     // respond when the intervention becomes active
171/// });
172///
173/// context.subscribe_to_event(|_context, _event: InterventionDeactivated| {
174///     // respond when the intervention becomes inactive
175/// });
176/// ```
177pub struct TogglingTriggerCriteria<AC, DC> {
178    activation_criterion: AC,
179    deactivation_criterion: DC,
180    initially_active: bool,
181    mode: TriggerMode,
182}
183
184impl<AC, DC> TogglingTriggerCriteria<AC, DC>
185where
186    AC: TriggerCriterion,
187    DC: TriggerCriterion,
188{
189    /// Create a toggling trigger criteria pair that starts inactive and uses repeating mode.
190    #[must_use]
191    pub fn new(activation_criterion: AC, deactivation_criterion: DC) -> Self {
192        Self {
193            activation_criterion,
194            deactivation_criterion,
195            initially_active: false,
196            mode: TriggerMode::Repeating,
197        }
198    }
199
200    /// Start the completed toggling trigger in the active state.
201    ///
202    /// See [`TogglingTrigger::initially_active`] for the runtime semantics.
203    #[must_use]
204    pub fn initially_active(mut self) -> Self {
205        self.initially_active = true;
206        self
207    }
208
209    /// Start the completed toggling trigger in the inactive state.
210    ///
211    /// This is the default state. See [`TogglingTrigger::initially_inactive`] for the runtime
212    /// semantics.
213    #[must_use]
214    pub fn initially_inactive(mut self) -> Self {
215        self.initially_active = false;
216        self
217    }
218
219    /// Run the completed toggling trigger through one active period and then disable it.
220    ///
221    /// This sets the mode of the completed [`TogglingTrigger`], not the mode of either component
222    /// criterion.
223    #[must_use]
224    pub fn once(mut self) -> Self {
225        self.mode = TriggerMode::Once;
226        self
227    }
228
229    /// Keep the completed toggling trigger enabled after deactivation so it can activate again.
230    ///
231    /// This is the default mode. This sets the mode of the completed [`TogglingTrigger`], not the
232    /// mode of either component criterion.
233    #[must_use]
234    pub fn repeating(mut self) -> Self {
235        self.mode = TriggerMode::Repeating;
236        self
237    }
238
239    /// Bind this pair of criteria to constructors for activation and deactivation events.
240    #[must_use]
241    pub fn emit_with<ActiveEv, InactiveEv, MakeActive, MakeInactive>(
242        self,
243        make_active_event: MakeActive,
244        make_inactive_event: MakeInactive,
245    ) -> TogglingTrigger<AC, DC, ActiveEv, InactiveEv, MakeActive, MakeInactive>
246    where
247        ActiveEv: IxaEvent,
248        InactiveEv: IxaEvent,
249        MakeActive: Fn(AC::Observation) -> ActiveEv + 'static,
250        MakeInactive: Fn(DC::Observation) -> InactiveEv + 'static,
251    {
252        TogglingTrigger {
253            activation_criterion: self.activation_criterion,
254            deactivation_criterion: self.deactivation_criterion,
255            make_active_event,
256            make_inactive_event,
257            initially_active: self.initially_active,
258            mode: self.mode,
259            _events: PhantomData,
260        }
261    }
262
263    /// Bind this pair of criteria to constant activation and deactivation event values.
264    #[must_use]
265    #[allow(clippy::type_complexity)]
266    pub fn emit_values<ActiveEv, InactiveEv>(
267        self,
268        active_event: ActiveEv,
269        inactive_event: InactiveEv,
270    ) -> TogglingTrigger<
271        AC,
272        DC,
273        ActiveEv,
274        InactiveEv,
275        impl Fn(AC::Observation) -> ActiveEv,
276        impl Fn(DC::Observation) -> InactiveEv,
277    >
278    where
279        ActiveEv: IxaEvent,
280        InactiveEv: IxaEvent,
281    {
282        self.emit_with(move |_| active_event, move |_| inactive_event)
283    }
284
285    /// Bind this pair of criteria to default-valued activation and deactivation events.
286    #[must_use]
287    #[allow(clippy::type_complexity)]
288    pub fn emit_defaults<ActiveEv, InactiveEv>(
289        self,
290    ) -> TogglingTrigger<
291        AC,
292        DC,
293        ActiveEv,
294        InactiveEv,
295        impl Fn(AC::Observation) -> ActiveEv,
296        impl Fn(DC::Observation) -> InactiveEv,
297    >
298    where
299        ActiveEv: IxaEvent + Default,
300        InactiveEv: IxaEvent + Default,
301    {
302        self.emit_with(|_| ActiveEv::default(), |_| InactiveEv::default())
303    }
304}
305
306/// Composite trigger that toggles between inactive and active states.
307///
308/// A complete installable trigger specification that emits activation and deactivation events when
309/// its paired criteria cause state changes.
310///
311/// See [TogglingTriggerCriteria] for complete documentation.
312pub struct TogglingTrigger<AC, DC, ActiveEv, InactiveEv, MakeActive, MakeInactive> {
313    activation_criterion: AC,
314    deactivation_criterion: DC,
315    make_active_event: MakeActive,
316    make_inactive_event: MakeInactive,
317    initially_active: bool,
318    mode: TriggerMode,
319    _events: PhantomData<fn() -> (ActiveEv, InactiveEv)>,
320}
321
322impl<AC, DC, ActiveEv, InactiveEv, MakeActive, MakeInactive>
323    TogglingTrigger<AC, DC, ActiveEv, InactiveEv, MakeActive, MakeInactive>
324where
325    AC: TriggerCriterion,
326    DC: TriggerCriterion,
327    ActiveEv: IxaEvent,
328    InactiveEv: IxaEvent,
329    MakeActive: Fn(AC::Observation) -> ActiveEv + 'static,
330    MakeInactive: Fn(DC::Observation) -> InactiveEv + 'static,
331{
332    /// Create a repeating toggling trigger that starts inactive.
333    ///
334    /// The activation criterion is accepted only while the trigger is inactive, and the
335    /// deactivation criterion is accepted only while the trigger is active. Repeating mode means
336    /// the trigger remains enabled after deactivation and can run through multiple active periods.
337    ///
338    /// The component criteria are used as match sources. They should usually be repeating criteria;
339    /// configuring a component criterion with its own `.once()` can consume that criterion on a
340    /// match that this toggling trigger ignores because it occurred in the wrong state.
341    #[must_use]
342    pub fn new(
343        activation_criterion: AC,
344        make_active_event: MakeActive,
345        deactivation_criterion: DC,
346        make_inactive_event: MakeInactive,
347    ) -> Self {
348        Self {
349            activation_criterion,
350            deactivation_criterion,
351            make_active_event,
352            make_inactive_event,
353            initially_active: false,
354            mode: TriggerMode::Repeating,
355            _events: PhantomData,
356        }
357    }
358
359    /// Start the trigger in the active state.
360    ///
361    /// An initially active trigger ignores activation matches until it first accepts a
362    /// deactivation match. If the toggling trigger is also configured with [`Self::once`], that
363    /// first accepted deactivation completes its one active period and permanently disables it.
364    #[must_use]
365    pub fn initially_active(mut self) -> Self {
366        self.initially_active = true;
367        self
368    }
369
370    /// Start the trigger in the inactive state.
371    ///
372    /// This is the default state and is provided as an explicit counterpart to
373    /// [`Self::initially_active`]. If the toggling trigger is configured with [`Self::once`], it can
374    /// accept one activation and then one deactivation before disabling itself.
375    #[must_use]
376    pub fn initially_inactive(mut self) -> Self {
377        self.initially_active = false;
378        self
379    }
380
381    /// Run through one active period and then permanently disable the toggling trigger.
382    ///
383    /// This method sets the mode of the toggling trigger itself. It does not change the mode of the
384    /// activation or deactivation criteria supplied to [`Self::new`]. For an initially inactive
385    /// trigger, one active period consists of one accepted activation followed by one accepted
386    /// deactivation. For an initially active trigger, the active period is already in progress, so
387    /// the first accepted deactivation disables the trigger.
388    #[must_use]
389    pub fn once(mut self) -> Self {
390        self.mode = TriggerMode::Once;
391        self
392    }
393
394    /// Keep the toggling trigger enabled after deactivation so it can activate again.
395    ///
396    /// This is the default mode. This method sets the mode of the toggling trigger itself. It does
397    /// not change the mode of the activation or deactivation criteria supplied to [`Self::new`].
398    #[must_use]
399    pub fn repeating(mut self) -> Self {
400        self.mode = TriggerMode::Repeating;
401        self
402    }
403}
404
405impl<AC, DC, ActiveEv, InactiveEv, MakeActive, MakeInactive> TriggerSpec
406    for TogglingTrigger<AC, DC, ActiveEv, InactiveEv, MakeActive, MakeInactive>
407where
408    AC: TriggerCriterion,
409    DC: TriggerCriterion,
410    ActiveEv: IxaEvent,
411    InactiveEv: IxaEvent,
412    MakeActive: Fn(AC::Observation) -> ActiveEv + 'static,
413    MakeInactive: Fn(DC::Observation) -> InactiveEv + 'static,
414{
415    fn install_in_context(self, context: &mut Context) {
416        let Self {
417            activation_criterion,
418            deactivation_criterion,
419            make_active_event,
420            make_inactive_event,
421            initially_active,
422            mode,
423            _events,
424        } = self;
425
426        let active = Rc::new(Cell::new(initially_active));
427        let enabled = Rc::new(Cell::new(true));
428
429        activation_criterion.install(context, {
430            let active = Rc::clone(&active);
431            let enabled = Rc::clone(&enabled);
432            move |context, observation| {
433                if enabled.get() && !active.get() {
434                    active.set(true);
435                    context.emit_event(make_active_event(observation));
436                }
437            }
438        });
439
440        deactivation_criterion.install(context, {
441            let active = Rc::clone(&active);
442            let enabled = Rc::clone(&enabled);
443            move |context, observation| {
444                if enabled.get() && active.get() {
445                    active.set(false);
446                    context.emit_event(make_inactive_event(observation));
447                    if mode == TriggerMode::Once {
448                        enabled.set(false);
449                    }
450                }
451            }
452        });
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use std::cell::{Cell, RefCell};
459    use std::rc::Rc;
460
461    use super::super::{
462        ContextTriggersExt, Direction, EntityCountTrigger, PropertyChangeTrigger,
463        PropertyValueCountTrigger, TimeTrigger,
464    };
465    use super::*;
466    use crate::entity::EntityId;
467    use crate::{define_entity, define_property, Context, ContextEntitiesExt, IxaEvent};
468
469    define_entity!(TogglePerson);
470    define_entity!(ToggleCase);
471
472    define_property!(
473        enum ToggleStatus {
474            Susceptible,
475            Infectious,
476        },
477        TogglePerson,
478        default_const = ToggleStatus::Susceptible
479    );
480
481    define_property!(struct ToggleAlive(bool), TogglePerson, default_const = ToggleAlive(true));
482
483    #[test]
484    fn toggling_trigger_gates_property_change_criteria() {
485        let mut context = Context::new();
486        let observed = Rc::new(RefCell::new(Vec::new()));
487
488        #[derive(IxaEvent)]
489        struct Activated {
490            previous: ToggleAlive,
491            current: ToggleAlive,
492        }
493
494        #[derive(IxaEvent)]
495        struct Deactivated {
496            previous: ToggleAlive,
497            current: ToggleAlive,
498        }
499
500        context.register_trigger(TogglingTrigger::new(
501            PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(false)),
502            |event| Activated {
503                previous: event.previous,
504                current: event.current,
505            },
506            PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(true)),
507            |event| Deactivated {
508                previous: event.previous,
509                current: event.current,
510            },
511        ));
512
513        context.subscribe_to_event({
514            let observed = Rc::clone(&observed);
515            move |_context, event: Activated| {
516                observed
517                    .borrow_mut()
518                    .push(("active", event.previous.0, event.current.0));
519            }
520        });
521        context.subscribe_to_event({
522            let observed = Rc::clone(&observed);
523            move |_context, event: Deactivated| {
524                observed
525                    .borrow_mut()
526                    .push(("inactive", event.previous.0, event.current.0));
527            }
528        });
529
530        let person = context.add_entity(TogglePerson).unwrap();
531        context.set_property(person, ToggleAlive(false));
532        context.set_property(person, ToggleAlive(false));
533        context.set_property(person, ToggleAlive(true));
534        context.set_property(person, ToggleAlive(true));
535        context.set_property(person, ToggleAlive(false));
536        context.execute();
537
538        assert_eq!(
539            *observed.borrow(),
540            vec![
541                ("active", true, false),
542                ("inactive", false, true),
543                ("active", true, false)
544            ]
545        );
546    }
547
548    #[test]
549    fn toggling_trigger_criteria_emit_with_builds_toggling_trigger() {
550        let mut context = Context::new();
551        let observed = Rc::new(RefCell::new(Vec::new()));
552
553        #[derive(IxaEvent)]
554        struct Activated {
555            previous: ToggleAlive,
556            current: ToggleAlive,
557        }
558
559        #[derive(IxaEvent)]
560        struct Deactivated {
561            previous: ToggleAlive,
562            current: ToggleAlive,
563        }
564
565        context.register_trigger(
566            TogglingTriggerCriteria::new(
567                PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(false)),
568                PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(true)),
569            )
570            .emit_with(
571                |event| Activated {
572                    previous: event.previous,
573                    current: event.current,
574                },
575                |event| Deactivated {
576                    previous: event.previous,
577                    current: event.current,
578                },
579            ),
580        );
581
582        context.subscribe_to_event({
583            let observed = Rc::clone(&observed);
584            move |_context, event: Activated| {
585                observed
586                    .borrow_mut()
587                    .push(("active", event.previous.0, event.current.0));
588            }
589        });
590        context.subscribe_to_event({
591            let observed = Rc::clone(&observed);
592            move |_context, event: Deactivated| {
593                observed
594                    .borrow_mut()
595                    .push(("inactive", event.previous.0, event.current.0));
596            }
597        });
598
599        let person = context.add_entity(TogglePerson).unwrap();
600        context.set_property(person, ToggleAlive(false));
601        context.set_property(person, ToggleAlive(true));
602        context.execute();
603
604        assert_eq!(
605            *observed.borrow(),
606            vec![("active", true, false), ("inactive", false, true)]
607        );
608    }
609
610    #[test]
611    fn toggling_trigger_can_start_active() {
612        let mut context = Context::new();
613        let observed = Rc::new(RefCell::new(Vec::new()));
614
615        #[derive(IxaEvent)]
616        struct Activated;
617
618        #[derive(IxaEvent)]
619        struct Deactivated;
620
621        context.register_trigger(
622            TogglingTrigger::new(
623                PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(false)),
624                |_| Activated,
625                PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(true)),
626                |_| Deactivated,
627            )
628            .initially_active(),
629        );
630
631        context.subscribe_to_event({
632            let observed = Rc::clone(&observed);
633            move |_context, _event: Activated| {
634                observed.borrow_mut().push("active");
635            }
636        });
637        context.subscribe_to_event({
638            let observed = Rc::clone(&observed);
639            move |_context, _event: Deactivated| {
640                observed.borrow_mut().push("inactive");
641            }
642        });
643
644        let person = context.add_entity(TogglePerson).unwrap();
645        context.set_property(person, ToggleAlive(false));
646        context.set_property(person, ToggleAlive(true));
647        context.set_property(person, ToggleAlive(false));
648        context.execute();
649
650        assert_eq!(*observed.borrow(), vec!["inactive", "active"]);
651    }
652
653    #[test]
654    fn toggling_trigger_once_disables_after_one_full_active_period() {
655        let mut context = Context::new();
656        let observed = Rc::new(RefCell::new(Vec::new()));
657
658        #[derive(IxaEvent)]
659        struct Activated;
660
661        #[derive(IxaEvent)]
662        struct Deactivated;
663
664        context.register_trigger(
665            TogglingTrigger::new(
666                PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(false)),
667                |_| Activated,
668                PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(true)),
669                |_| Deactivated,
670            )
671            .once(),
672        );
673
674        context.subscribe_to_event({
675            let observed = Rc::clone(&observed);
676            move |_context, _event: Activated| {
677                observed.borrow_mut().push("active");
678            }
679        });
680        context.subscribe_to_event({
681            let observed = Rc::clone(&observed);
682            move |_context, _event: Deactivated| {
683                observed.borrow_mut().push("inactive");
684            }
685        });
686
687        let person = context.add_entity(TogglePerson).unwrap();
688        context.set_property(person, ToggleAlive(true));
689        context.set_property(person, ToggleAlive(false));
690        context.set_property(person, ToggleAlive(false));
691        context.set_property(person, ToggleAlive(true));
692        context.set_property(person, ToggleAlive(false));
693        context.set_property(person, ToggleAlive(true));
694        context.execute();
695
696        assert_eq!(*observed.borrow(), vec!["active", "inactive"]);
697    }
698
699    #[test]
700    fn toggling_trigger_once_initially_active_disables_after_first_deactivation() {
701        let mut context = Context::new();
702        let observed = Rc::new(RefCell::new(Vec::new()));
703
704        #[derive(IxaEvent)]
705        struct Activated;
706
707        #[derive(IxaEvent)]
708        struct Deactivated;
709
710        context.register_trigger(
711            TogglingTrigger::new(
712                PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(false)),
713                |_| Activated,
714                PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(true)),
715                |_| Deactivated,
716            )
717            .initially_active()
718            .once(),
719        );
720
721        context.subscribe_to_event({
722            let observed = Rc::clone(&observed);
723            move |_context, _event: Activated| {
724                observed.borrow_mut().push("active");
725            }
726        });
727        context.subscribe_to_event({
728            let observed = Rc::clone(&observed);
729            move |_context, _event: Deactivated| {
730                observed.borrow_mut().push("inactive");
731            }
732        });
733
734        let person = context.add_entity(TogglePerson).unwrap();
735        context.set_property(person, ToggleAlive(false));
736        context.set_property(person, ToggleAlive(true));
737        context.set_property(person, ToggleAlive(false));
738        context.set_property(person, ToggleAlive(true));
739        context.execute();
740
741        assert_eq!(*observed.borrow(), vec!["inactive"]);
742    }
743
744    #[test]
745    fn toggling_trigger_criteria_emit_values_uses_constant_events() {
746        let mut context = Context::new();
747        let observed = Rc::new(RefCell::new(Vec::new()));
748
749        #[derive(IxaEvent)]
750        struct Activated;
751
752        #[derive(IxaEvent)]
753        struct Deactivated;
754
755        context.register_trigger(
756            TogglingTriggerCriteria::new(
757                PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(false)),
758                PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(true)),
759            )
760            .emit_values(Activated, Deactivated),
761        );
762
763        context.subscribe_to_event({
764            let observed = Rc::clone(&observed);
765            move |_context, _event: Activated| {
766                observed.borrow_mut().push("active");
767            }
768        });
769        context.subscribe_to_event({
770            let observed = Rc::clone(&observed);
771            move |_context, _event: Deactivated| {
772                observed.borrow_mut().push("inactive");
773            }
774        });
775
776        let person = context.add_entity(TogglePerson).unwrap();
777        context.set_property(person, ToggleAlive(false));
778        context.set_property(person, ToggleAlive(true));
779        context.execute();
780
781        assert_eq!(*observed.borrow(), vec!["active", "inactive"]);
782    }
783
784    #[test]
785    fn toggling_trigger_criteria_emit_defaults_uses_default_events() {
786        let mut context = Context::new();
787        let observed = Rc::new(RefCell::new(Vec::new()));
788
789        #[derive(Default, IxaEvent)]
790        struct Activated;
791
792        #[derive(Default, IxaEvent)]
793        struct Deactivated;
794
795        context.register_trigger(
796            TogglingTriggerCriteria::new(
797                PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(false)),
798                PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(true)),
799            )
800            .emit_defaults::<Activated, Deactivated>(),
801        );
802
803        context.subscribe_to_event({
804            let observed = Rc::clone(&observed);
805            move |_context, _event: Activated| {
806                observed.borrow_mut().push("active");
807            }
808        });
809        context.subscribe_to_event({
810            let observed = Rc::clone(&observed);
811            move |_context, _event: Deactivated| {
812                observed.borrow_mut().push("inactive");
813            }
814        });
815
816        let person = context.add_entity(TogglePerson).unwrap();
817        context.set_property(person, ToggleAlive(false));
818        context.set_property(person, ToggleAlive(true));
819        context.execute();
820
821        assert_eq!(*observed.borrow(), vec!["active", "inactive"]);
822    }
823
824    #[test]
825    fn toggling_trigger_criteria_transfers_initial_state_and_mode() {
826        let mut context = Context::new();
827        let observed = Rc::new(RefCell::new(Vec::new()));
828
829        #[derive(IxaEvent)]
830        struct Activated;
831
832        #[derive(IxaEvent)]
833        struct Deactivated;
834
835        context.register_trigger(
836            TogglingTriggerCriteria::new(
837                PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(false)),
838                PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(true)),
839            )
840            .initially_active()
841            .once()
842            .emit_values(Activated, Deactivated),
843        );
844
845        context.subscribe_to_event({
846            let observed = Rc::clone(&observed);
847            move |_context, _event: Activated| {
848                observed.borrow_mut().push("active");
849            }
850        });
851        context.subscribe_to_event({
852            let observed = Rc::clone(&observed);
853            move |_context, _event: Deactivated| {
854                observed.borrow_mut().push("inactive");
855            }
856        });
857
858        let person = context.add_entity(TogglePerson).unwrap();
859        context.set_property(person, ToggleAlive(false));
860        context.set_property(person, ToggleAlive(true));
861        context.set_property(person, ToggleAlive(false));
862        context.set_property(person, ToggleAlive(true));
863        context.execute();
864
865        assert_eq!(*observed.borrow(), vec!["inactive"]);
866    }
867
868    #[test]
869    fn toggling_trigger_supports_distinct_observation_and_event_types() {
870        let mut context = Context::new();
871        let observed_case_count = Rc::new(Cell::new(0));
872        let observed_time = Rc::new(Cell::new(0.0));
873
874        #[derive(IxaEvent)]
875        struct CasesActivated {
876            count: usize,
877        }
878
879        #[derive(IxaEvent)]
880        struct TimeDeactivated {
881            time: f64,
882        }
883
884        context.register_trigger(TogglingTrigger::new(
885            EntityCountTrigger::<ToggleCase>::increases_to(1),
886            |event| CasesActivated { count: event.count },
887            TimeTrigger::at(1.0),
888            |event| TimeDeactivated { time: event.time },
889        ));
890
891        context.subscribe_to_event({
892            let observed_case_count = Rc::clone(&observed_case_count);
893            move |_context, event: CasesActivated| {
894                observed_case_count.set(event.count);
895            }
896        });
897        context.subscribe_to_event({
898            let observed_time = Rc::clone(&observed_time);
899            move |_context, event: TimeDeactivated| {
900                observed_time.set(event.time);
901            }
902        });
903
904        context.add_entity(ToggleCase).unwrap();
905        context.execute();
906
907        assert_eq!(observed_case_count.get(), 1);
908        assert_eq!(observed_time.get(), 1.0);
909    }
910
911    #[test]
912    fn toggling_trigger_applies_property_value_count_hysteresis() {
913        let mut context = Context::new();
914        let observed = Rc::new(RefCell::new(Vec::new()));
915
916        #[derive(IxaEvent)]
917        struct Activated {
918            count: usize,
919            direction: Direction,
920        }
921
922        #[derive(IxaEvent)]
923        struct Deactivated {
924            count: usize,
925            direction: Direction,
926        }
927
928        context.register_trigger(TogglingTrigger::new(
929            PropertyValueCountTrigger::<TogglePerson, ToggleStatus>::changes_to(
930                ToggleStatus::Infectious,
931                2,
932            ),
933            |event| Activated {
934                count: event.count,
935                direction: event.direction,
936            },
937            PropertyValueCountTrigger::<TogglePerson, ToggleStatus>::changes_to(
938                ToggleStatus::Infectious,
939                4,
940            ),
941            |event| Deactivated {
942                count: event.count,
943                direction: event.direction,
944            },
945        ));
946
947        context.subscribe_to_event({
948            let observed = Rc::clone(&observed);
949            move |_context, event: Activated| {
950                observed
951                    .borrow_mut()
952                    .push(("active", event.count, event.direction));
953            }
954        });
955        context.subscribe_to_event({
956            let observed = Rc::clone(&observed);
957            move |_context, event: Deactivated| {
958                observed
959                    .borrow_mut()
960                    .push(("inactive", event.count, event.direction));
961            }
962        });
963
964        let first = context.add_entity(TogglePerson).unwrap();
965        let second = context.add_entity(TogglePerson).unwrap();
966        let third = context.add_entity(TogglePerson).unwrap();
967        let fourth = context.add_entity(TogglePerson).unwrap();
968
969        context.add_plan(0.1, move |context| {
970            context.set_property(first, ToggleStatus::Infectious);
971        });
972        context.add_plan(0.2, move |context| {
973            context.set_property(second, ToggleStatus::Infectious);
974        });
975        context.add_plan(0.3, move |context| {
976            context.set_property(second, ToggleStatus::Susceptible);
977        });
978        context.add_plan(0.4, move |context| {
979            context.set_property(second, ToggleStatus::Infectious);
980        });
981        context.add_plan(0.5, move |context| {
982            context.set_property(third, ToggleStatus::Infectious);
983        });
984        context.add_plan(0.6, move |context| {
985            context.set_property(fourth, ToggleStatus::Infectious);
986        });
987        context.add_plan(0.7, move |context| {
988            context.set_property(fourth, ToggleStatus::Susceptible);
989        });
990        context.add_plan(0.8, move |context| {
991            context.set_property(fourth, ToggleStatus::Infectious);
992        });
993        context.add_plan(0.9, move |context| {
994            context.set_property(fourth, ToggleStatus::Susceptible);
995        });
996        context.add_plan(1.0, move |context| {
997            context.set_property(third, ToggleStatus::Susceptible);
998        });
999        context.add_plan(1.1, move |context| {
1000            context.set_property(first, ToggleStatus::Susceptible);
1001        });
1002        context.add_plan(1.2, move |context| {
1003            context.set_property(first, ToggleStatus::Infectious);
1004        });
1005        context.add_plan(1.3, move |context| {
1006            context.set_property(third, ToggleStatus::Infectious);
1007        });
1008        context.add_plan(1.4, move |context| {
1009            context.set_property(fourth, ToggleStatus::Infectious);
1010        });
1011
1012        context.execute();
1013
1014        assert_eq!(
1015            *observed.borrow(),
1016            vec![
1017                ("active", 2, Direction::Increasing),
1018                ("inactive", 4, Direction::Increasing),
1019                ("active", 2, Direction::Decreasing),
1020                ("inactive", 4, Direction::Increasing),
1021            ]
1022        );
1023    }
1024
1025    #[test]
1026    fn toggling_trigger_can_report_entity_ids_from_observations() {
1027        let mut context = Context::new();
1028        let observed = Rc::new(Cell::new(None));
1029
1030        #[derive(IxaEvent)]
1031        struct Activated {
1032            entity_id: EntityId<TogglePerson>,
1033        }
1034
1035        #[derive(IxaEvent)]
1036        struct Deactivated;
1037
1038        context.register_trigger(TogglingTrigger::new(
1039            PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(false)),
1040            |event| Activated {
1041                entity_id: event.entity_id,
1042            },
1043            PropertyChangeTrigger::<TogglePerson, ToggleAlive>::to(ToggleAlive(true)),
1044            |_| Deactivated,
1045        ));
1046
1047        context.subscribe_to_event({
1048            let observed = Rc::clone(&observed);
1049            move |_context, event: Activated| {
1050                observed.set(Some(event.entity_id));
1051            }
1052        });
1053
1054        let person = context.add_entity(TogglePerson).unwrap();
1055        context.set_property(person, ToggleAlive(false));
1056        context.execute();
1057
1058        assert_eq!(observed.get(), Some(person));
1059    }
1060}