made-core 0.7.7

Domain core of MADE: entities, value objects, events, ports. No IO.
Documentation
use std::collections::BTreeSet;

use crate::entities::{CeremonyDefinition, CeremonyInstance};
use crate::error::DomainError;
use crate::value_objects::{
    RoleAction, RoleId, StateExecution, StepExecutionRecord, StepId, StepStatus,
};

impl CeremonyInstance {
    pub fn resolved_step_role(
        &self,
        definition: &CeremonyDefinition,
        step_id: &StepId,
    ) -> Result<RoleId, DomainError> {
        self.resolve_step_role(definition, step_id, None)
            .map(|(role, _)| role)
    }

    pub(super) fn resolve_step_role(
        &self,
        definition: &CeremonyDefinition,
        step_id: &StepId,
        requested_role: Option<&RoleId>,
    ) -> Result<(RoleId, bool), DomainError> {
        let step = definition.step(step_id).ok_or(DomainError::NotFound {
            what: "ceremony_instance.step",
        })?;
        let Some(binding) = step.dynamic_role_binding() else {
            let role = requested_role
                .cloned()
                .map_or_else(|| definition.role_id_for_step(step_id), Ok)?;
            self.require_role(definition, &role, &RoleAction::step(step_id.clone()))?;
            self.require_role_is_available(definition, step_id, &role)?;
            return Ok((role, false));
        };

        let raw = self
            .context
            .get(binding.context_key())
            .ok_or(DomainError::NotFound {
                what: "ceremony_step.role_from.context_key",
            })?;
        let raw = raw.as_str().ok_or(DomainError::InvariantViolated {
            reason: "dynamic role context value must be a string",
        })?;
        let resolved = RoleId::new(raw)?;
        if !binding.allows(&resolved) {
            return Err(DomainError::InvariantViolated {
                reason: "dynamic role is outside the step allow-list",
            });
        }
        if requested_role.is_some_and(|requested| requested != &resolved) {
            return Err(DomainError::InvariantViolated {
                reason: "requested step role differs from the dynamic role",
            });
        }
        self.require_role(definition, &resolved, &RoleAction::step(step_id.clone()))?;
        self.require_role_is_available(definition, step_id, &resolved)?;
        Ok((resolved, true))
    }

    fn require_role_is_available(
        &self,
        definition: &CeremonyDefinition,
        step_id: &StepId,
        role: &RoleId,
    ) -> Result<(), DomainError> {
        let concurrent = definition
            .state(&self.current_state)
            .is_some_and(|state| state.execution() == StateExecution::Concurrent);
        if concurrent
            && self
                .roles_assigned_to_other_steps(definition, step_id)?
                .contains(role)
        {
            return Err(DomainError::InvariantViolated {
                reason: "role is already assigned to another step in this state iteration",
            });
        }
        Ok(())
    }

    fn roles_assigned_to_other_steps(
        &self,
        definition: &CeremonyDefinition,
        step_id: &StepId,
    ) -> Result<BTreeSet<RoleId>, DomainError> {
        let current = self.current_state_iteration;
        let mut roles = BTreeSet::new();
        for (other_id, record) in &self.step_records {
            if other_id != step_id
                && record.state_iteration() == current
                && record.state_visit() == self.current_state_visit
                && definition
                    .step(other_id)
                    .is_some_and(|step| step.state_id() == &self.current_state)
            {
                roles.extend(reserved_role(definition, other_id, record)?);
            }
        }
        for (other_id, history) in &self.step_record_history {
            if other_id == step_id {
                continue;
            }
            for record in history {
                if record.state_iteration() == current
                    && record.state_visit() == self.current_state_visit
                    && definition
                        .step(other_id)
                        .is_some_and(|step| step.state_id() == &self.current_state)
                {
                    roles.extend(reserved_role(definition, other_id, record)?);
                }
            }
        }
        Ok(roles)
    }
}

fn reserved_role(
    definition: &CeremonyDefinition,
    step_id: &StepId,
    record: &StepExecutionRecord,
) -> Result<Option<RoleId>, DomainError> {
    if let Some(role) = record.claimed_role() {
        return Ok(Some(role.clone()));
    }
    let static_non_pending = record.status() != StepStatus::Pending
        && definition
            .step(step_id)
            .is_some_and(|step| step.dynamic_role_binding().is_none());
    if static_non_pending {
        definition.role_id_for_step(step_id).map(Some)
    } else {
        Ok(None)
    }
}