123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357 |
- /*
- Package fsm defines an embeddable finite state machine.
- In its initial version, it is a straight port of the 2012 PHP
- OSInet\Finite_State_Machine\Machine available in https://code.osinet.fr/fgm/php__phplib.git
- See http://wiki.audean.com/fsm/fsm for details.
- Copyright (c) 2023 Ouest Systèmes Informatiques
- License GPL-3.0
- */
- package fsm
- import (
- "errors"
- "fmt"
- "log"
- "golang.org/x/exp/maps"
- "golang.org/x/exp/slices"
- )
- type (
- Event any
- EventMode int
- State string
- Transition map[Event][2]Result
- )
- const (
- Version string = "12D22"
- IdleEvent = "idle" // must not change name: method fIdle depends on it
- InitState State = "init"
- FinalState State = "final"
- )
- const (
- EventInvalid EventMode = iota // Should not be seen
- EventNormal // Processes events
- EventQueue // Queue events for later use
- EventSink // Throw away events
- )
- // Machine is a composable state machine.
- //
- // Applications must create an actual FSM composing such a Machine, in which they will:
- // - define the fTransitions table, usually in their constructor
- // - invoke the parent constructor to set up the FSM from the transitions table
- // - create "f_foo" functions for each "foo" event, except "idle", which is builtin.
- //
- // Applications may:
- // - disable event processing by setting $this->event to one of the fsm::EVENT_* constants
- // - disable post-event actions by setting $this->allowsActions to false
- // - disable the builtin idle event by setting $this->idleWork to false
- // - query the current state by using $this->getState()
- // - send an idle event by using $this->idle()
- // - submit any event (including idle) by using $this->ApplyEvent($eventName)
- type Machine struct {
- AllowActions bool
- EventMode EventMode
- IdleWork bool
- Queue []any
- State State
- Transitions map[State]Transition
- }
- // checkTransitions verifies than a transitions graph has been defined.
- //
- // Since applications are not expected to modify the machine once initialized,
- // it is not meant to be used beyond New.
- func (m Machine) checkTransitions() error {
- if len(m.Transitions) == 0 {
- return errors.New("no FSM processing is allowed without a transitions table")
- }
- if _, ok := m.Transitions[InitState]; ok {
- return errors.New("transitions map does not have any initial state")
- }
- return nil
- }
- // New initializes the FSM to the first state in the transitions table
- func New(transitions map[State]Transition) (*Machine, error) {
- m := Machine{
- AllowActions: true,
- IdleWork: true,
- EventMode: EventNormal,
- Queue: []any{}, // Queue is the event queue for EVENT_QUEUE mode
- State: InitState, // State holds the current state of the machine
- Transitions: transitions, // Transitions holds the transitions table
- }
- if err := m.checkTransitions(); err != nil {
- return nil, err
- }
- return &m, nil
- }
- // AcceptedEvents returns the list of events accepted in the current state.
- func (m Machine) AcceptedEvents() []Event {
- t, ok := m.Transitions[m.State]
- if !ok {
- return nil
- }
- events := maps.Keys(t)
- return events
- }
- // AcceptedOutcomes returns the list of outcomes accepted in the current state for the given event
- func (m Machine) AcceptedOutcomes(e Event) ([2]Result, error) {
- res, ok := m.Transitions[m.State][e]
- if !ok {
- return [2]Result{}, fmt.Errorf("event %q not accepted in state %q", e, m.State)
- }
- return res, nil
- }
- // IsEventAllowed verifies whether the given event is accepted in the current state
- func (m Machine) IsEventAllowed(e Event) bool {
- t := m.Transitions[m.State]
- isIt := slices.Contains(maps.Keys(t), e)
- return isIt
- }
- // IsOutcomeAllowed checks whether a given outcome available for a given event, considering the current state.
- func (m Machine) IsOutcomeAllowed(e Event, outcome Result) bool {
- if !m.IsEventAllowed(e) {
- return false
- }
- res, err := m.AcceptedOutcomes(e)
- if err != nil {
- return false
- }
- isIt := outcome == res[0] || outcome == res[1]
- return isIt
- }
- // ApplyEvent applies both the given event and the resulting event chain if applicable.
- func (m *Machine) ApplyEvent(e Event) Result {
- var res Result
- for {
- var next Event
- res = m.ApplySimpleEvent(e)
- if m.AllowActions {
- next = res.Action
- } else {
- next = nil
- }
- if next != nil {
- break
- }
- }
- return res
- }
- // ApplySimpleEvent is a helper for ApplyEvent that does not implement the post-transition action
- //
- // See ApplyEvent
- func (m *Machine) ApplySimpleEvent(e Event) Result {
- currentState := m.State
- if e == IdleEvent && !m.IdleWork {
- return Result{}
- }
- if !m.IsEventAllowed(e) {
- log.Fatalf("Event %q not allowed in current state %q.\n", e, currentState)
- }
- return Result{}
- }
- $outcomes = $this- > getAcceptedOutcomes($eventName)
- $methodName = "event$eventName"
- if !method_exists($this, $methodName)) {
- die (func_name().": missing method "
- .get_class($this)."::$methodName. Aborting.\n")
- }
- $outcome = $this- >$methodName()
- if !in_array($outcome, $outcomes)) {
- throw new \Exception(func_name()
- .": event guard. Transition on \"$eventName\" return invalid outcome: "
- .print_r($outcome, true)
- ." for state $this->fState\n")
- }
- $transition = &$this- > fTransitions[$currentState][$eventName][$outcome]
- $result = new Result (
- $outcome,
- $transition[0],
- $transition[1]
- )
- if (isset($result->fsmState)) {
- $this->fState = $result->fsmState
- }
- /* print_r($this->fTransitions[$currentState][$eventName]);
- var_dump($result); */
- if (!isset($outcome)) {
- $outcome = 'NULL'
- }
- /*
- echo func_name()
- . ": $currentState: " . $eventName . '[' . $outcome . ']'
- . " -> $this->fState / " . $result->fsmAction . PHP_EOL;
- */
- return result
- }
- /**
- * Default event.
- *
- * @return boolean
- */
- public function fIdle() {
- return TRUE
- }
- /**
- * Apply an fsm::IDLE_EVENT event. Do not confuse with fIdle !
- *
- * @return Result
- */
- public function idle() {
- return $this->applyEvent(self::IDLE_EVENT)
- }
- /**
- * return the current operating mode of the FSM
- * @return int
- */
- public function getEventMode() {
- return $this->fEventMode
- }
- /**
- * set the current operating mode for the FSM
- *
- * @param int $mode fsm::EVENT_* constants
- * @return int $mode fsm::EVENT_* constants
- */
- public function setEventMode($mode) {
- switch ($mode) {
- case self::EVENT_NORMAL:
- if (count($this->fQueue) > 0) {
- while (($event = array_shift($this->fQueue)) != = NULL) {
- $this->applyEvent($event)
- }
- }
- break
- // Nothing special to do
- case self::EVENT_QUEUE:
- break
- // Empty queue if needed
- case self::EVENT_SINK:
- if (count($this->fQueue) > 0) {
- $this->fQueue = array()
- }
- break
- default:
- throw new Exception("Trying to set unknown FSM mode $mode")
- }
- $this->fEventMode = $mode
- return $this->fEventMode
- }
- /**
- * Load the fTransitions table from an external resource.
- *
- * @param string $url
- * Optional: defaults to the name of the class, . ".xml"
- */
- public function loadFsm($url = NULL) {
- $osd = FALSE // on screen display (debug)
- if ($osd) {
- echo "Loading FSM from $url\n"
- }
- if (!isset($url)) {
- $url = get_class($this).".xml"
- }
- $fsm = simplexml_load_file($url)
- $fsmVersion = (string) $fsm['fsm_version']
- if ($fsmVersion != = '1.3') {
- die("Revision $fsmVersion of schema is not supported.\n")
- }
- $this->idleWork = ($fsm['idle_work'] == 1)
- $this->allowActions = ($fsm['allow_actions'] == 1)
- $this->fTransitions = array()
- $t = &$this->fTransitions
- // (string) casts in this loop are required: RHS is a SimpleXMLElement
- foreach ($fsm->state as $state) {
- $id = (string) $state['id']
- if ($osd) {
- echo "State $id :\n"
- }
- $t[$id] = array()
- switch ($id) {
- case self::INIT_STATE:
- if ($osd) {
- echo " Initial state\n"
- }
- break
- case self::FINAL_STATE:
- if ($osd) {
- echo " Final state\n"
- }
- break
- }
- foreach ($state->event as $event) {
- $name = (string) $event['name']
- if ($osd) {
- echo " Event $name"
- }
- if (!isset($event['type'])) {
- $event['type'] = 'void'
- }
- $eventType = (string) $event['type']
- if ($osd) {
- echo ", type $eventType\n"
- }
- foreach ($event as $next) {
- if ($event['type'] == 'void') {
- $next['result'] = 'always'
- $result = null
- } else {
- $result = (string) $next['result']
- }
- if (!isset($next['state'])) {
- $next['state'] = (string) $state['id']
- }
- if ($osd) {
- echo " Next(".$next['result'].') = '.$next['state']
- if (isset($next['action'])) {
- echo " + event ".$next['action']
- }
- echo PHP_EOL
- }
- $t[$id][$name][$result] = array(
- (string) $next['state'],
- (string) $next['action'])
- }
- }
- }
- }
- }
|