made_core/entities/ceremony_instance/
transitions.rs1use crate::entities::ceremony_commands::ApplyTransition;
2use crate::entities::CeremonyCommand;
3use crate::value_objects::{CeremonyTransition, GuardCondition};
4
5use super::{
6 CeremonyDefinition, CeremonyEvent, CeremonyInstance, DomainError, OffsetDateTime, RoleId,
7 StateId, TransitionTrigger,
8};
9
10impl CeremonyInstance {
11 #[must_use]
13 pub fn transition_is_enabled(
14 &self,
15 definition: &CeremonyDefinition,
16 transition: &CeremonyTransition,
17 ) -> bool {
18 self.transition_budget_allows(definition, transition)
19 && self.transition_requirements_are_satisfied(definition, transition)
20 }
21
22 #[must_use]
27 pub fn transition_requirements_are_satisfied(
28 &self,
29 definition: &CeremonyDefinition,
30 transition: &CeremonyTransition,
31 ) -> bool {
32 self.state_repeat_permits_transition(definition)
33 && self.guards_are_satisfied(definition, transition)
34 && self
35 .require_interventions_resolved_before_entering(definition, transition.to())
36 .is_ok()
37 }
38
39 #[must_use]
40 pub fn guard_is_satisfied_for_transition(
41 &self,
42 definition: &CeremonyDefinition,
43 transition: &CeremonyTransition,
44 guard: &crate::value_objects::CeremonyGuard,
45 ) -> bool {
46 match guard.condition() {
47 GuardCondition::ChildrenCompleted(condition) => {
48 definition
49 .step(condition.step_id())
50 .is_some_and(|step| step.state_id() == transition.from())
51 && self.children_completed_guard_is_satisfied(condition)
52 }
53 _ => definition.guard_is_satisfied_for_transition(
54 guard,
55 transition,
56 &self.step_records,
57 &self.context,
58 ),
59 }
60 }
61
62 pub(super) fn guards_are_satisfied(
63 &self,
64 definition: &CeremonyDefinition,
65 transition: &CeremonyTransition,
66 ) -> bool {
67 definition.repeat_requirements_are_satisfied_for_transition(transition, &self.step_records)
68 && transition.required_guards().iter().all(|name| {
69 definition.guards().get(name).is_some_and(|guard| {
70 self.guard_is_satisfied_for_transition(definition, transition, guard)
71 })
72 })
73 }
74
75 pub(super) fn require_transition_budget(
76 &self,
77 definition: &CeremonyDefinition,
78 transition: &CeremonyTransition,
79 ) -> Result<(), DomainError> {
80 if definition.max_transitions().is_some_and(|limit| {
81 u64::try_from(self.transitions.len()).unwrap_or(u64::MAX) >= u64::from(limit.get())
82 }) {
83 return Err(DomainError::InvariantViolated {
84 reason: "ceremony transition limit exhausted",
85 });
86 }
87 if definition.max_bounces().is_some_and(|limit| {
88 u64::try_from(self.exact_edge_count(transition)).unwrap_or(u64::MAX)
89 >= u64::from(limit.get())
90 }) {
91 return Err(DomainError::InvariantViolated {
92 reason: "ceremony transition bounce limit exhausted",
93 });
94 }
95 Ok(())
96 }
97
98 fn transition_budget_allows(
99 &self,
100 definition: &CeremonyDefinition,
101 transition: &CeremonyTransition,
102 ) -> bool {
103 self.require_transition_budget(definition, transition)
104 .is_ok()
105 }
106
107 fn exact_edge_count(&self, transition: &CeremonyTransition) -> usize {
108 self.transitions
109 .iter()
110 .filter(|record| {
111 record.from_state() == transition.from()
112 && record.trigger() == transition.trigger()
113 && record.to_state() == transition.to()
114 })
115 .count()
116 }
117
118 #[must_use]
119 pub fn transition_is_enabled_at(
120 &self,
121 definition: &CeremonyDefinition,
122 transition: &CeremonyTransition,
123 now: OffsetDateTime,
124 ) -> bool {
125 !self.has_live_step_leases_at(definition, now)
126 && self.transition_is_enabled(definition, transition)
127 }
128
129 pub(super) fn require_interventions_resolved_before_entering(
130 &self,
131 definition: &CeremonyDefinition,
132 state_id: &StateId,
133 ) -> Result<(), DomainError> {
134 if definition.is_terminal_state(state_id)
135 && self
136 .interventions
137 .iter()
138 .any(|item| item.status().is_open())
139 {
140 return Err(DomainError::InvariantViolated {
141 reason: "ceremony cannot enter a terminal state with open interventions",
142 });
143 }
144 Ok(())
145 }
146
147 pub fn apply_transition_as(
148 &mut self,
149 definition: &CeremonyDefinition,
150 role_id: &RoleId,
151 trigger: &TransitionTrigger,
152 now: OffsetDateTime,
153 ) -> Result<StateId, DomainError> {
154 self.move_on(definition, trigger, Some(role_id.clone()), now)
155 }
156
157 pub fn apply_transition(
158 &mut self,
159 definition: &CeremonyDefinition,
160 trigger: &TransitionTrigger,
161 now: OffsetDateTime,
162 ) -> Result<StateId, DomainError> {
163 self.move_on(definition, trigger, None, now)
164 }
165
166 fn move_on(
169 &mut self,
170 definition: &CeremonyDefinition,
171 trigger: &TransitionTrigger,
172 applied_by: Option<RoleId>,
173 now: OffsetDateTime,
174 ) -> Result<StateId, DomainError> {
175 let command = CeremonyCommand::ApplyTransition(ApplyTransition {
176 role_id: applied_by,
177 trigger: trigger.clone(),
178 now,
179 });
180 let events = self.decide(&command, definition)?;
181 let to_state = events
182 .iter()
183 .find_map(|event| match event {
184 CeremonyEvent::TransitionApplied(applied) => {
185 Some(applied.transition.to_state().clone())
186 }
187 _ => None,
188 })
189 .ok_or(DomainError::InvariantViolated {
190 reason: "moving decides a transition",
191 })?;
192 self.apply_all(&events);
193 Ok(to_state)
194 }
195}