Skip to main content

ixa/triggers/
toggling_trigger.rs

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