RustArena

Step 5 of 6

SHOP-240: the order workflow state machine

SHOP-240: Enforce the order lifecycle

Orders have been shipped without payment and refunded after delivery, because any code could set order.status = "shipped". Model the workflow so that only legal transitions are possible, and every illegal one gets a clear error.

An enum state machine (module 42) makes illegal states unrepresentable: each state is a variant that carries exactly the data that exists in that state (a tracking number only once shipped), and one function decides every transition. Matching on the (state, event) tuple lists the legal moves explicitly, and a final catch-all arm rejects everything else:

#[derive(Debug)]
enum Door {
    Open,
    Closed,
    Locked { code: u32 },
}

#[derive(Debug)]
enum Action {
    Close,
    Lock(u32),
    Unlock(u32),
    Open,
}

fn step(door: Door, action: &Action) -> Result<Door, (Door, String)> {
    match (door, action) {
        (Door::Open, Action::Close) => Ok(Door::Closed),
        (Door::Closed, Action::Open) => Ok(Door::Open),
        (Door::Closed, Action::Lock(code)) => Ok(Door::Locked { code: *code }),
        (Door::Locked { code }, Action::Unlock(attempt)) if *attempt == code => Ok(Door::Closed),
        (door @ Door::Locked { .. }, Action::Unlock(_)) => Err((door, "wrong code".to_string())),
        (door, action) => {
            let why = format!("cannot {action:?} while {door:?}");
            Err((door, why))
        }
    }
}

fn main() {
    let actions = [Action::Close, Action::Lock(1234), Action::Open, Action::Unlock(1111), Action::Unlock(1234), Action::Open];
    let mut door = Door::Open;
    for action in &actions {
        door = match step(door, action) {
            Ok(next) => next,
            Err((unchanged, why)) => {
                println!("rejected {action:?}: {why}");
                unchanged
            }
        };
    }
    println!("final: {door:?}");
}
rejected Open: cannot Open while Locked { code: 1234 }
rejected Unlock(1111): wrong code
final: Open

Common mistakes

Your task

Implement the order workflow. The seed defines OrderState, Event and TransitionError (with thiserror). Write:

Challenge 2

SUP-118: Replay the support ticket log

The helpdesk exports every action as a line of text. We need the final state of every ticket, plus a list of actions the old system wrongly accepted.

Standard input has one action per line: TICKET ACTION [NAME]. A ticket that appears for the first time starts open. The actions and their legal transitions:

For every illegal action, print rejected: TICKET ACTION (ticket is STATE) as you go, and leave the ticket unchanged. At the end, print every ticket in order of its id with TICKET: STATE, where STATE is open, in progress (NAME), resolved or closed. Ignore lines that don't have a known action or are missing the name for assign (they don't create a ticket either).

Model the state as an enum with a describe() method, and keep the tickets in a BTreeMap<String, TicketState>.

Challenge 3

CMS-77: Articles must be reviewed before publishing

An intern published a draft by calling publish() directly. Make that impossible at compile time.

Use the typestate pattern (module 34): a separate type per state, where each method consumes self and returns the next state's type, so calling a method in the wrong state doesn't compile. Write:

There's no publish on Draft at all: the only way to a Published is through review.

Previous: OPS-330: validate the deploy config Next: REVIEW-9: refactor for code review