made_core/entities/ceremony_instance/
role_resolution.rs1use std::collections::BTreeSet;
2
3use crate::entities::{CeremonyDefinition, CeremonyInstance};
4use crate::error::DomainError;
5use crate::value_objects::{
6 RoleAction, RoleId, StateExecution, StepExecutionRecord, StepId, StepStatus,
7};
8
9impl CeremonyInstance {
10 pub fn resolved_step_role(
11 &self,
12 definition: &CeremonyDefinition,
13 step_id: &StepId,
14 ) -> Result<RoleId, DomainError> {
15 self.resolve_step_role(definition, step_id, None)
16 .map(|(role, _)| role)
17 }
18
19 pub(super) fn resolve_step_role(
20 &self,
21 definition: &CeremonyDefinition,
22 step_id: &StepId,
23 requested_role: Option<&RoleId>,
24 ) -> Result<(RoleId, bool), DomainError> {
25 let step = definition.step(step_id).ok_or(DomainError::NotFound {
26 what: "ceremony_instance.step",
27 })?;
28 let Some(binding) = step.dynamic_role_binding() else {
29 let role = requested_role
30 .cloned()
31 .map_or_else(|| definition.role_id_for_step(step_id), Ok)?;
32 self.require_role(definition, &role, &RoleAction::step(step_id.clone()))?;
33 self.require_role_is_available(definition, step_id, &role)?;
34 return Ok((role, false));
35 };
36
37 let raw = self
38 .context
39 .get(binding.context_key())
40 .ok_or(DomainError::NotFound {
41 what: "ceremony_step.role_from.context_key",
42 })?;
43 let raw = raw.as_str().ok_or(DomainError::InvariantViolated {
44 reason: "dynamic role context value must be a string",
45 })?;
46 let resolved = RoleId::new(raw)?;
47 if !binding.allows(&resolved) {
48 return Err(DomainError::InvariantViolated {
49 reason: "dynamic role is outside the step allow-list",
50 });
51 }
52 if requested_role.is_some_and(|requested| requested != &resolved) {
53 return Err(DomainError::InvariantViolated {
54 reason: "requested step role differs from the dynamic role",
55 });
56 }
57 self.require_role(definition, &resolved, &RoleAction::step(step_id.clone()))?;
58 self.require_role_is_available(definition, step_id, &resolved)?;
59 Ok((resolved, true))
60 }
61
62 fn require_role_is_available(
63 &self,
64 definition: &CeremonyDefinition,
65 step_id: &StepId,
66 role: &RoleId,
67 ) -> Result<(), DomainError> {
68 let concurrent = definition
69 .state(&self.current_state)
70 .is_some_and(|state| state.execution() == StateExecution::Concurrent);
71 if concurrent
72 && self
73 .roles_assigned_to_other_steps(definition, step_id)?
74 .contains(role)
75 {
76 return Err(DomainError::InvariantViolated {
77 reason: "role is already assigned to another step in this state iteration",
78 });
79 }
80 Ok(())
81 }
82
83 fn roles_assigned_to_other_steps(
84 &self,
85 definition: &CeremonyDefinition,
86 step_id: &StepId,
87 ) -> Result<BTreeSet<RoleId>, DomainError> {
88 let current = self.current_state_iteration;
89 let mut roles = BTreeSet::new();
90 for (other_id, record) in &self.step_records {
91 if other_id != step_id
92 && record.state_iteration() == current
93 && record.state_visit() == self.current_state_visit
94 && definition
95 .step(other_id)
96 .is_some_and(|step| step.state_id() == &self.current_state)
97 {
98 roles.extend(reserved_role(definition, other_id, record)?);
99 }
100 }
101 for (other_id, history) in &self.step_record_history {
102 if other_id == step_id {
103 continue;
104 }
105 for record in history {
106 if record.state_iteration() == current
107 && record.state_visit() == self.current_state_visit
108 && definition
109 .step(other_id)
110 .is_some_and(|step| step.state_id() == &self.current_state)
111 {
112 roles.extend(reserved_role(definition, other_id, record)?);
113 }
114 }
115 }
116 Ok(roles)
117 }
118}
119
120fn reserved_role(
121 definition: &CeremonyDefinition,
122 step_id: &StepId,
123 record: &StepExecutionRecord,
124) -> Result<Option<RoleId>, DomainError> {
125 if let Some(role) = record.claimed_role() {
126 return Ok(Some(role.clone()));
127 }
128 let static_non_pending = record.status() != StepStatus::Pending
129 && definition
130 .step(step_id)
131 .is_some_and(|step| step.dynamic_role_binding().is_none());
132 if static_non_pending {
133 definition.role_id_for_step(step_id).map(Some)
134 } else {
135 Ok(None)
136 }
137}