package fsm // StateID is a state identifier. Machine with string state IDs can have *a lot of* states. type StateID string // NilStateID is a noop state. Machine won't do anything for this transition. const NilStateID = StateID("") // MachineControls is a fragment of IMachine implementation. This one can Move between the states or Reset the machine. // It may fail with an error which should be handled by the IState implementation. type MachineControls[T any] interface { Move(StateID, *T) error // MoveForHandle is the same as Move but it also triggers Handle immediately for the next state. MoveForHandle(StateID, *T) error Reset() } // MachineState returns machine state. This state should be immutable. type MachineState[T any] interface { // State returns underlying state. This state SHOULD NOT be modified since there is no locking performed. State() *T } // MachineControlsWithState is self-explanatory. type MachineControlsWithState[T any] interface { MachineControls[T] MachineState[T] } // IState is a State interface. This contract enforces that any state implementation should have three methods: // Enter, Handle and Exit. The first one is called right after the machine has moved to the state, the second one is // called when handling some state input, the last one is called right before leaving to a next state (which happens // if Handle has called Move on MachineControls. type IState[T any] interface { // ID should return state identifier. ID() StateID // Enter is a state enter callback. Can be used to perform some sort of input query or to move to another // state immediately. Also, if Enter fails the machine won't move to the state and MachineControls's Move will // return an error which was returned from Enter. Enter(*T, MachineControls[T]) error // Handle is called when receiving some sort of input response. This one's signature is nearly identical to Enter, // but it can wait for some sort of input (standard input? webhook) without locking inside the callback // while Enter cannot do that. Handle(*T, MachineControls[T]) // Exit is called right before leaving the state. It can be used to modify state's payload (T) or for some other // miscellaneous tasks. // Note: calling Exit doesn't mean that the machine will really transition to the next state. // The next state's Enter callback can return an error which will reset the Machine to default state & payload. Exit(*T) } // ErrorState is the Machine's fatal error handler which you should implement yourself. // // Error state is used by Machine in case of a fatal error (for example, for invalid state ID). // You can use ErrorState to make some kind of fatal error response like "Internal error has occurred" // or something similar. Also, ErrorState is special because neither Enter nor Exit exists in it. // // Machine without ErrorState will not do anything in case of fatal errors. type ErrorState[T any] interface { Handle(err error, current StateID, next StateID, payload *T, machine MachineControls[T]) } // State is the Machine's state. This implementation doesn't do anything and only helps with the // actual IState implementation (you don't need to write empty Enter and Exit callback if you don't use them). type State[T any] struct { Payload *T } // ID panics because you need to implement it. func (s *State[T]) ID() StateID { panic("implement ID() StateID method for your state") } // Enter here will immediately move the Machine to empty state. func (s *State[T]) Enter(payload *T, machine MachineControls[T]) error { return machine.Move(NilStateID, payload) } // Handle here will immediately move the Machine to empty state. func (s *State[T]) Handle(payload *T, machine MachineControls[T]) { _ = machine.Move(NilStateID, payload) } // Exit won't do anything. func (s *State[T]) Exit(*T) {}