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::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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38#[non_exhaustive]
39pub enum TransitionError {
40 InvalidTransition {
42 source: EditorialState,
43 target: EditorialState,
44 },
45 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}