made_core/entities/ceremony_instance/
transitions.rs1use crate::entities::ceremony_commands::ApplyTransition;
2use crate::entities::CeremonyCommand;
3use crate::value_objects::CeremonyTransition;
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 && definition.guards_are_satisfied(transition, &self.step_records, &self.context)
34 && self
35 .require_interventions_resolved_before_entering(definition, transition.to())
36 .is_ok()
37 }
38
39 pub(super) fn require_transition_budget(
40 &self,
41 definition: &CeremonyDefinition,
42 transition: &CeremonyTransition,
43 ) -> Result<(), DomainError> {
44 if definition.max_transitions().is_some_and(|limit| {
45 u64::try_from(self.transitions.len()).unwrap_or(u64::MAX) >= u64::from(limit.get())
46 }) {
47 return Err(DomainError::InvariantViolated {
48 reason: "ceremony transition limit exhausted",
49 });
50 }
51 if definition.max_bounces().is_some_and(|limit| {
52 u64::try_from(self.exact_edge_count(transition)).unwrap_or(u64::MAX)
53 >= u64::from(limit.get())
54 }) {
55 return Err(DomainError::InvariantViolated {
56 reason: "ceremony transition bounce limit exhausted",
57 });
58 }
59 Ok(())
60 }
61
62 fn transition_budget_allows(
63 &self,
64 definition: &CeremonyDefinition,
65 transition: &CeremonyTransition,
66 ) -> bool {
67 self.require_transition_budget(definition, transition)
68 .is_ok()
69 }
70
71 fn exact_edge_count(&self, transition: &CeremonyTransition) -> usize {
72 self.transitions
73 .iter()
74 .filter(|record| {
75 record.from_state() == transition.from()
76 && record.trigger() == transition.trigger()
77 && record.to_state() == transition.to()
78 })
79 .count()
80 }
81
82 #[must_use]
83 pub fn transition_is_enabled_at(
84 &self,
85 definition: &CeremonyDefinition,
86 transition: &CeremonyTransition,
87 now: OffsetDateTime,
88 ) -> bool {
89 !self.has_live_step_leases_at(definition, now)
90 && self.transition_is_enabled(definition, transition)
91 }
92
93 pub(super) fn require_interventions_resolved_before_entering(
94 &self,
95 definition: &CeremonyDefinition,
96 state_id: &StateId,
97 ) -> Result<(), DomainError> {
98 if definition.is_terminal_state(state_id)
99 && self
100 .interventions
101 .iter()
102 .any(|item| item.status().is_open())
103 {
104 return Err(DomainError::InvariantViolated {
105 reason: "ceremony cannot enter a terminal state with open interventions",
106 });
107 }
108 Ok(())
109 }
110
111 pub fn apply_transition_as(
112 &mut self,
113 definition: &CeremonyDefinition,
114 role_id: &RoleId,
115 trigger: &TransitionTrigger,
116 now: OffsetDateTime,
117 ) -> Result<StateId, DomainError> {
118 self.move_on(definition, trigger, Some(role_id.clone()), now)
119 }
120
121 pub fn apply_transition(
122 &mut self,
123 definition: &CeremonyDefinition,
124 trigger: &TransitionTrigger,
125 now: OffsetDateTime,
126 ) -> Result<StateId, DomainError> {
127 self.move_on(definition, trigger, None, now)
128 }
129
130 fn move_on(
133 &mut self,
134 definition: &CeremonyDefinition,
135 trigger: &TransitionTrigger,
136 applied_by: Option<RoleId>,
137 now: OffsetDateTime,
138 ) -> Result<StateId, DomainError> {
139 let command = CeremonyCommand::ApplyTransition(ApplyTransition {
140 role_id: applied_by,
141 trigger: trigger.clone(),
142 now,
143 });
144 let events = self.decide(&command, definition)?;
145 let to_state = events
146 .iter()
147 .find_map(|event| match event {
148 CeremonyEvent::TransitionApplied(applied) => {
149 Some(applied.transition.to_state().clone())
150 }
151 _ => None,
152 })
153 .ok_or(DomainError::InvariantViolated {
154 reason: "moving decides a transition",
155 })?;
156 self.apply_all(&events);
157 Ok(to_state)
158 }
159}