Skip to main content

made_core/entities/ceremony_instance/
role_resolution.rs

1use 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}