State Machine Actions#

Actions are returned from state event handlers and are the normal way of communicating back to the state machine what to do next. The only requirement on an Action type is to have an Execute() method, that gets the state machine instance, the current state and the event being processed.

Additional type management utility methods are added to action classes as needed to simplify the unit testing of the states and actions. They are in no way used in the basic functioning of a state machine which totally relies on static compile-time types and rules.

template<typename TargetState>
struct TransitionTo#

An action type that represents a state transition from the current state to the target state specified as a template parameter.

This action supports rich states which may contain data and may have some complex logic that utilizes that data to decide on the next state transition. When being executed by the state machine, it is provided with the current state (through the state machine instance), the newly selected state and the event that triggered the transition.

Furthermore, it calls the OnLeave method on the previous state and OnEnter on the destination state, provided they are in the states interface (it uses SFINAE to detect if such a method exists).

The requirement on the OnLeave method is to return a State object that can only be one of Continue, Terminate or TerminateWithError. The OnEnter method on the other hand can return any of the possible values of Status.

Example

  struct TransitionEvent {};

  struct ExampleMockState {
    // NOLINTNEXTLINE
    MOCK_METHOD(Status, OnEnter, (const TransitionEvent &), ());
    // NOLINTNEXTLINE
    MOCK_METHOD(Status, OnLeave, (const TransitionEvent &), ());
  };

  struct FirstState;
  struct SecondState;
  using Machine = StateMachine<FirstState, SecondState>;

  struct FirstState : Will<On<TransitionEvent, TransitionTo<SecondState>>> {
    explicit FirstState(std::shared_ptr<ExampleMockState> mock)
        : mock_{std::move(mock)} {
    }
    // We only implement the OnLeave method, but that's ok.
    // The action will only call whatever is implemented.
    [[nodiscard]] auto OnLeave(const TransitionEvent &event) const -> Status {
      return mock_->OnLeave(event);
    }

  private:
    const std::shared_ptr<ExampleMockState> mock_;
  };

  struct SecondState : Will<ByDefault<DoNothing>> {
    explicit SecondState(std::shared_ptr<ExampleMockState> mock)
        : mock_{std::move(mock)} {
    }
    // We only implement the OnEnter method, but that's ok.
    // The action will only call whatever is implemented.
    [[nodiscard]] auto OnEnter(const TransitionEvent &event) const -> Status {
      return mock_->OnEnter(event);
    }

  private:
    const std::shared_ptr<ExampleMockState> mock_;
  };

  const auto mock_initial_state = std::make_shared<ExampleMockState>();
  const auto mock_another_state = std::make_shared<ExampleMockState>();
  Machine machine{
      FirstState{mock_initial_state}, SecondState{mock_another_state}};

  TransitionEvent event{};

  EXPECT_CALL(*mock_initial_state, OnLeave(Ref(event)))
      .Times(1)
      .WillOnce(Return(Continue{}));
  EXPECT_CALL(*mock_another_state, OnEnter(Ref(event)))
      .Times(1)
      .WillOnce(Return(Status{Continue{}}));
  machine.Handle(event);
  ASSERT_THAT(machine.IsIn<SecondState>(), IsTrue());
A state can implement a Handle method that returns a TransitionTo object constructed with a piece of data (std::any object) that will be passed to the next state when its onEnter method is called. The next state can opt to ignore that data by implementing onEnter(Event event) only or can opt to use the data by implementing onEnter(Event event, std::any data).

Example of data passing between states

  struct TransitionEvent {};

  struct ExampleMockState {
    // NOLINTNEXTLINE
    MOCK_METHOD(Status, OnEnter, (const TransitionEvent &, int), ());
    // NOLINTNEXTLINE
    MOCK_METHOD(Status, OnLeave, (const TransitionEvent &), ());
  };

  struct FirstState;
  struct SecondState;
  using Machine = StateMachine<FirstState, SecondState>;

  struct FirstState {
    explicit FirstState(std::shared_ptr<ExampleMockState> mock)
        : mock_{std::move(mock)} {
    }
    // We only implement the OnLeave method, but that's ok.
    // The action will only call whatever is implemented.
    [[nodiscard]] auto OnLeave(const TransitionEvent &event) const -> Status {
      return mock_->OnLeave(event);
    }

    // This special handler allows for passing data via the
    // transition object to the next state which can be consumed
    // if that state implements `onEnter(event, data)`
    [[nodiscard]] static auto Handle(const TransitionEvent & /*event*/)
        -> TransitionTo<SecondState> {
      return TransitionTo<SecondState>{1};
    }

  private:
    const std::shared_ptr<ExampleMockState> mock_;
  };

  struct SecondState : Will<ByDefault<DoNothing>> {
    explicit SecondState(std::shared_ptr<ExampleMockState> mock)
        : mock_{std::move(mock)} {
    }
    // This implementation expects data to be passed from the
    // previous state.
    [[nodiscard]] auto OnEnter(
        const TransitionEvent &event, std::any data) const -> Status {
      EXPECT_THAT(std::any_cast<int>(data), Eq(1));
      // Mocking methods with std::any arguments will fail to compile with clang
      // and there is no fix for it from gmock. Just specifically cast the
      // std::any before forwarding to the mock.
      return mock_->OnEnter(event, std::any_cast<int>(data));
    }

  private:
    const std::shared_ptr<ExampleMockState> mock_;
  };

  const auto mock_initial_state = std::make_shared<ExampleMockState>();
  const auto mock_another_state = std::make_shared<ExampleMockState>();
  Machine machine{
      FirstState{mock_initial_state}, SecondState{mock_another_state}};

  TransitionEvent event{};

  EXPECT_CALL(*mock_initial_state, OnLeave(Ref(event)))
      .Times(1)
      .WillOnce(Return(Continue{}));
  EXPECT_CALL(*mock_another_state, OnEnter(Ref(event), ::testing::_))
      .Times(1)
      .WillOnce(Return(Status{Continue{}}));
  machine.Handle(event);
  ASSERT_THAT(machine.IsIn<SecondState>(), IsTrue());
struct DoNothing#

Helper action representing the situation where an event is ignored and the currents state stays as is.

Example

  struct DoNothingEvent {};
  struct TestState {
    static auto Handle(const DoNothingEvent & /*event*/) -> DoNothing {
      // Returning the `DoNothing` action will result in no side effects
      // from the state machine calling the `Execute` method of the action.
      return DoNothing{};
    }
  };

  StateMachine<TestState> machine{TestState{}};
  machine.Handle(DoNothingEvent{});
A typical use of this action is in combination with ByDefault to make a state that by default does nothing with any event it receives and therefore only focus on implementing those event handlers that make sense for that particular state.

struct ReportError#

The action to be used to report an error from an event handler.

Throwing an exception from event handlers to report an error is not recommended as it will break the interface of the event handlers (expected to return an action to be executed) and make the unit testing more complicated.

Instead, the event handler would use this action to report the error, providing the error explanatory message in the form of the action’s data.

template<typename ...Actions>
struct OneOf#

Base type for states modeling the situation where the result of an event can be one of a set of possible actions.

Since actions are returned from state Handle methods and can have only one return type, this class multiplexes the return types using the variadic template parameter pack.

Example

  struct SpecialEvent {};

  struct FirstState;
  struct SecondState;
  using Machine = StateMachine<FirstState, SecondState>;

  struct SecondState : Will<ByDefault<DoNothing>> {};

  struct SpecialAction {
    static auto Execute(Machine & /*machine*/, FirstState & /*state*/,
        const SpecialEvent & /*event*/) -> Status {
      return Continue{};
    }
  };

  struct FirstState {
    explicit FirstState(bool transition) : transition_{transition} {
    }

    // This handler has two alternate paths. We use the `OneOf`
    // helper to still be able to return a single action type
    [[nodiscard]] auto Handle(const SpecialEvent & /*event*/) const
        -> OneOf<TransitionTo<SecondState>, SpecialAction> {
      if (transition_) {
        return TransitionTo<SecondState>{};
      }
      return SpecialAction{};
    }
    const bool transition_;
  };

  Machine machine1{FirstState{true}, SecondState{}};
  try {
    machine1.Handle(SpecialEvent{});
  } catch (...) {
  }
  ASSERT_THAT(machine1.IsIn<SecondState>(), IsTrue());

  Machine machine2{FirstState{false}, SecondState{}};
  machine2.Handle(SpecialEvent{});
  ASSERT_THAT(machine2.IsIn<FirstState>(), IsTrue());
template<typename Action>
struct Maybe : public asap::fsm::OneOf<Action, DoNothing>#

Base type for states modeling the situation where when an event is triggered, we want to either do one thing or do nothing at all.

Example

  struct SpecialEvent {};

  struct FirstState;
  struct SecondState;
  using Machine = StateMachine<FirstState, SecondState>;

  struct SecondState : Will<ByDefault<DoNothing>> {};

  struct SpecialAction {
    static auto Execute(Machine & /*machine*/, FirstState & /*state*/,
        const SpecialEvent & /*event*/) -> Status {
      return Continue{};
    }
  };

  struct FirstState {
    explicit FirstState(bool transition) : transition_{transition} {
    }

    // This handler has two alternate paths. We use the `OneOf`
    // helper to still be able to return a single action type
    [[nodiscard]] auto Handle(const SpecialEvent & /*event*/) const
        -> OneOf<TransitionTo<SecondState>, SpecialAction> {
      if (transition_) {
        return TransitionTo<SecondState>{};
      }
      return SpecialAction{};
    }
    const bool transition_;
  };

  Machine machine1{FirstState{true}, SecondState{}};
  try {
    machine1.Handle(SpecialEvent{});
  } catch (...) {
  }
  ASSERT_THAT(machine1.IsIn<SecondState>(), IsTrue());

  Machine machine2{FirstState{false}, SecondState{}};
  machine2.Handle(SpecialEvent{});
  ASSERT_THAT(machine2.IsIn<FirstState>(), IsTrue());
template<typename T>
struct is_one_of : public std::false_type#

A concept to identify if a particular type is a OneOf<…Actions> type.

template<typename T, typename ...Ts>
constexpr auto asap::fsm::supports_alternative() -> bool#

A concept to identify OneOf types with a specific alternative.