Skip to main content

syncweb_core/editorial/
workflow.rs

1use super::{EditorialRole, EditorialState};
2
3/// Transition a collection from `current` to `target`, checking that `role` is
4/// sufficient.
5///
6/// # Errors
7///
8/// Returns `Err` when the transition is not allowed for the given role.
9pub fn assert_transition(
10    current: Option<EditorialState>,
11    target: EditorialState,
12    role: EditorialRole,
13) -> Result<EditorialState, TransitionError> {
14    let source = current.unwrap_or_default();
15    if !source.can_transition_to(target) {
16        return Err(TransitionError::InvalidTransition { source, target });
17    }
18    if role_can_transition(role, source, target) {
19        Ok(target)
20    } else {
21        Err(TransitionError::InsufficientRole { role, source, target })
22    }
23}
24
25const fn role_can_transition(role: EditorialRole, source: EditorialState, target: EditorialState) -> bool {
26    use EditorialState as S;
27    match (source, target) {
28        (S::Proposed | S::ChangesRequested | S::Retracted, S::Draft)
29        | (S::Draft | S::ChangesRequested | S::Approved, S::Proposed) => role.can_initiate(),
30        (S::Proposed, S::InReview) | (S::InReview, S::ChangesRequested | S::Approved) => role.can_review(),
31        (S::Approved | S::Archived, S::Published) => role.can_publish(),
32        (S::Published, S::Archived | S::Retracted) => role.can_withdraw(),
33        _ => false,
34    }
35}
36
37/// Error returned when a state transition is rejected.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39#[non_exhaustive]
40pub enum TransitionError {
41    /// The state machine does not permit a direct jump.
42    InvalidTransition {
43        source: EditorialState,
44        target: EditorialState,
45    },
46    /// The actor's role is insufficient for the requested transition.
47    InsufficientRole {
48        role: EditorialRole,
49        source: EditorialState,
50        target: EditorialState,
51    },
52}
53
54impl core::fmt::Display for TransitionError {
55    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
56        match self {
57            Self::InvalidTransition { source, target } => {
58                write!(f, "invalid editorial transition from {source:?} to {target:?}")
59            }
60            Self::InsufficientRole { role, source, target } => {
61                write!(f, "role {role:?} cannot transition {source:?} → {target:?}")
62            }
63        }
64    }
65}