Mercurial > prosody-hg
comparison util/fsm.lua @ 13019:8a2f75e38eb2
util.fsm: New utility lib for finite state machines
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Thu, 17 Mar 2022 17:45:27 +0000 |
| parents | |
| children | 9c13c11b199d |
comparison
equal
deleted
inserted
replaced
| 13018:9ed4a8502c54 | 13019:8a2f75e38eb2 |
|---|---|
| 1 local events = require "util.events"; | |
| 2 | |
| 3 local fsm_methods = {}; | |
| 4 local fsm_mt = { __index = fsm_methods }; | |
| 5 | |
| 6 local function is_fsm(o) | |
| 7 local mt = getmetatable(o); | |
| 8 return mt == fsm_mt; | |
| 9 end | |
| 10 | |
| 11 local function notify_transition(fire_event, transition_event) | |
| 12 local ret; | |
| 13 ret = fire_event("transition", transition_event); | |
| 14 if ret ~= nil then return ret; end | |
| 15 if transition_event.from ~= transition_event.to then | |
| 16 ret = fire_event("leave/"..transition_event.from, transition_event); | |
| 17 if ret ~= nil then return ret; end | |
| 18 end | |
| 19 ret = fire_event("transition/"..transition_event.name, transition_event); | |
| 20 if ret ~= nil then return ret; end | |
| 21 end | |
| 22 | |
| 23 local function notify_transitioned(fire_event, transition_event) | |
| 24 if transition_event.to ~= transition_event.from then | |
| 25 fire_event("enter/"..transition_event.to, transition_event); | |
| 26 end | |
| 27 if transition_event.name then | |
| 28 fire_event("transitioned/"..transition_event.name, transition_event); | |
| 29 end | |
| 30 fire_event("transitioned", transition_event); | |
| 31 end | |
| 32 | |
| 33 local function do_transition(name) | |
| 34 return function (self, attr) | |
| 35 local new_state = self.fsm.states[self.state][name] or self.fsm.states["*"][name]; | |
| 36 if not new_state then | |
| 37 return error(("Invalid state transition: %s cannot %s"):format(self.state, name)); | |
| 38 end | |
| 39 | |
| 40 local transition_event = { | |
| 41 instance = self; | |
| 42 | |
| 43 name = name; | |
| 44 to = new_state; | |
| 45 to_attr = attr; | |
| 46 | |
| 47 from = self.state; | |
| 48 from_attr = self.state_attr; | |
| 49 }; | |
| 50 | |
| 51 local fire_event = self.fsm.events.fire_event; | |
| 52 local ret = notify_transition(fire_event, transition_event); | |
| 53 if ret ~= nil then return nil, ret; end | |
| 54 | |
| 55 self.state = new_state; | |
| 56 self.state_attr = attr; | |
| 57 | |
| 58 notify_transitioned(fire_event, transition_event); | |
| 59 return true; | |
| 60 end; | |
| 61 end | |
| 62 | |
| 63 local function new(desc) | |
| 64 local self = setmetatable({ | |
| 65 default_state = desc.default_state; | |
| 66 events = events.new(); | |
| 67 }, fsm_mt); | |
| 68 | |
| 69 -- states[state_name][transition_name] = new_state_name | |
| 70 local states = { ["*"] = {} }; | |
| 71 if desc.default_state then | |
| 72 states[desc.default_state] = {}; | |
| 73 end | |
| 74 self.states = states; | |
| 75 | |
| 76 local instance_methods = {}; | |
| 77 self._instance_mt = { __index = instance_methods }; | |
| 78 | |
| 79 for _, transition in ipairs(desc.transitions or {}) do | |
| 80 local from_states = transition.from; | |
| 81 if type(from_states) ~= "table" then | |
| 82 from_states = { from_states }; | |
| 83 end | |
| 84 for _, from in ipairs(from_states) do | |
| 85 if not states[from] then | |
| 86 states[from] = {}; | |
| 87 end | |
| 88 if not states[transition.to] then | |
| 89 states[transition.to] = {}; | |
| 90 end | |
| 91 if states[from][transition.name] then | |
| 92 return error(("Duplicate transition in FSM specification: %s from %s"):format(transition.name, from)); | |
| 93 end | |
| 94 states[from][transition.name] = transition.to; | |
| 95 end | |
| 96 | |
| 97 -- Add public method to trigger this transition | |
| 98 instance_methods[transition.name] = do_transition(transition.name); | |
| 99 end | |
| 100 | |
| 101 if desc.state_handlers then | |
| 102 for state_name, handler in pairs(desc.state_handlers) do | |
| 103 self.events.add_handler("enter/"..state_name, handler); | |
| 104 end | |
| 105 end | |
| 106 | |
| 107 if desc.transition_handlers then | |
| 108 for transition_name, handler in pairs(desc.transition_handlers) do | |
| 109 self.events.add_handler("transition/"..transition_name, handler); | |
| 110 end | |
| 111 end | |
| 112 | |
| 113 if desc.handlers then | |
| 114 self.events.add_handlers(desc.handlers); | |
| 115 end | |
| 116 | |
| 117 return self; | |
| 118 end | |
| 119 | |
| 120 function fsm_methods:init(state_name, state_attr) | |
| 121 local initial_state = assert(state_name or self.default_state, "no initial state specified"); | |
| 122 if not self.states[initial_state] then | |
| 123 return error("Invalid initial state: "..initial_state); | |
| 124 end | |
| 125 local instance = setmetatable({ | |
| 126 fsm = self; | |
| 127 state = initial_state; | |
| 128 state_attr = state_attr; | |
| 129 }, self._instance_mt); | |
| 130 | |
| 131 if initial_state ~= self.default_state then | |
| 132 local fire_event = self.events.fire_event; | |
| 133 notify_transitioned(fire_event, { | |
| 134 instance = instance; | |
| 135 | |
| 136 to = initial_state; | |
| 137 to_attr = state_attr; | |
| 138 | |
| 139 from = self.default_state; | |
| 140 }); | |
| 141 end | |
| 142 | |
| 143 return instance; | |
| 144 end | |
| 145 | |
| 146 function fsm_methods:is_instance(o) | |
| 147 local mt = getmetatable(o); | |
| 148 return mt == self._instance_mt; | |
| 149 end | |
| 150 | |
| 151 return { | |
| 152 new = new; | |
| 153 is_fsm = is_fsm; | |
| 154 }; |
