Step 5: Guards

Some transitions are conditional: you can only checkout if the cart has items, only ship if stock is reserved. A guard is a predicate attached to a transition; even when the table allows the event, a failing guard blocks it. fire already consults _passes_guard; implement it and the way to register guards.

  • add_guard(state, event, predicate): attach a predicate to a transition
  • _passes_guard(event): return True if there is no guard for the current (state, event), otherwise the result of calling the predicate with the order
Hints
Starter code
class Order:
    TRANSITIONS = {
        "cart": {"checkout": "pending"},
        "pending": {"pay": "paid", "cancel": "cancelled"},
        "paid": {"ship": "shipped", "refund": "cancelled"},
        "shipped": {"deliver": "delivered"},
        "delivered": {},
        "cancelled": {},
    }

    def __init__(self):
        self.state = "cart"
        self.history = ["cart"]
        self.items = 0
        self._enter_callbacks = {}
        self._guards = {}                # (state, event) -> predicate

    def fire(self, event):
        allowed = self.TRANSITIONS.get(self.state, {})
        if event not in allowed:
            return False
        if not self._passes_guard(event):
            return False
        self.state = allowed[event]
        self.history.append(self.state)
        for callback in self._enter_callbacks.get(self.state, []):
            callback(self)
        return True

    def add_guard(self, state, event, predicate):
        """Attach a predicate that must pass for this transition to fire."""
        # TODO: self._guards[(state, event)] = predicate
        pass

    def _passes_guard(self, event):
        """True if there is no guard, else the predicate's result for this order."""
        # TODO:
        #   predicate = self._guards.get((self.state, event))
        #   return predicate is None or predicate(self)
        pass

    def on_enter(self, state, callback):
        self._enter_callbacks.setdefault(state, []).append(callback)

    def can(self, event):
        return event in self.TRANSITIONS.get(self.state, {})

    def available_events(self):
        return list(self.TRANSITIONS.get(self.state, {}).keys())

    def is_terminal(self):
        return len(self.TRANSITIONS.get(self.state, {})) == 0
Write it on a laptop

The editor and the test runner need a wide screen. Open this page on a desktop browser and your progress will be waiting.