syncweb_core/editorial/
workflow.rs1use super::{EditorialRole, EditorialState};
2
3pub 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39#[non_exhaustive]
40pub enum TransitionError {
41 InvalidTransition {
43 source: EditorialState,
44 target: EditorialState,
45 },
46 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}