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::Draft) | (S::Draft | S::ChangesRequested | S::Approved, S::Proposed) => role.can_initiate(),
29        (S::Proposed, S::InReview) | (S::InReview, S::ChangesRequested | S::Approved) => role.can_review(),
30        (S::Approved | S::Archived | S::Draft, S::Published) => role.can_publish(),
31        (S::Published, S::Archived | S::Retracted) => role.can_withdraw(),
32        _ => false,
33    }
34}
35
36/// Error returned when a state transition is rejected.
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38#[non_exhaustive]
39pub enum TransitionError {
40    /// The state machine does not permit a direct jump.
41    InvalidTransition {
42        source: EditorialState,
43        target: EditorialState,
44    },
45    /// The actor's role is insufficient for the requested transition.
46    InsufficientRole {
47        role: EditorialRole,
48        source: EditorialState,
49        target: EditorialState,
50    },
51}
52
53impl core::fmt::Display for TransitionError {
54    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
55        match self {
56            Self::InvalidTransition { source, target } => {
57                write!(f, "invalid editorial transition from {source:?} to {target:?}")
58            }
59            Self::InsufficientRole { role, source, target } => {
60                write!(f, "role {role:?} cannot transition {source:?} → {target:?}")
61            }
62        }
63    }
64}