aboutsummaryrefslogtreecommitdiffstats
path: root/util
diff options
context:
space:
mode:
authorMatthew Wild <mwild1@gmail.com>2022-03-17 17:45:27 +0000
committerMatthew Wild <mwild1@gmail.com>2022-03-17 17:45:27 +0000
commitb06dd038cd796b93f9fa7a578ba0f279feb2f2b0 (patch)
treeab2e3947dff6c5ca33db458dacb4e019fffc94b3 /util
parentaa0d6f297d27f1c3ae1a94d05d9855af6dda4e45 (diff)
downloadprosody-b06dd038cd796b93f9fa7a578ba0f279feb2f2b0.tar.gz
prosody-b06dd038cd796b93f9fa7a578ba0f279feb2f2b0.zip
util.fsm: New utility lib for finite state machines
Diffstat (limited to 'util')
-rw-r--r--util/fsm.lua154
1 files changed, 154 insertions, 0 deletions
diff --git a/util/fsm.lua b/util/fsm.lua
new file mode 100644
index 00000000..94a543d1
--- /dev/null
+++ b/util/fsm.lua
@@ -0,0 +1,154 @@
+local events = require "util.events";
+
+local fsm_methods = {};
+local fsm_mt = { __index = fsm_methods };
+
+local function is_fsm(o)
+ local mt = getmetatable(o);
+ return mt == fsm_mt;
+end
+
+local function notify_transition(fire_event, transition_event)
+ local ret;
+ ret = fire_event("transition", transition_event);
+ if ret ~= nil then return ret; end
+ if transition_event.from ~= transition_event.to then
+ ret = fire_event("leave/"..transition_event.from, transition_event);
+ if ret ~= nil then return ret; end
+ end
+ ret = fire_event("transition/"..transition_event.name, transition_event);
+ if ret ~= nil then return ret; end
+end
+
+local function notify_transitioned(fire_event, transition_event)
+ if transition_event.to ~= transition_event.from then
+ fire_event("enter/"..transition_event.to, transition_event);
+ end
+ if transition_event.name then
+ fire_event("transitioned/"..transition_event.name, transition_event);
+ end
+ fire_event("transitioned", transition_event);
+end
+
+local function do_transition(name)
+ return function (self, attr)
+ local new_state = self.fsm.states[self.state][name] or self.fsm.states["*"][name];
+ if not new_state then
+ return error(("Invalid state transition: %s cannot %s"):format(self.state, name));
+ end
+
+ local transition_event = {
+ instance = self;
+
+ name = name;
+ to = new_state;
+ to_attr = attr;
+
+ from = self.state;
+ from_attr = self.state_attr;
+ };
+
+ local fire_event = self.fsm.events.fire_event;
+ local ret = notify_transition(fire_event, transition_event);
+ if ret ~= nil then return nil, ret; end
+
+ self.state = new_state;
+ self.state_attr = attr;
+
+ notify_transitioned(fire_event, transition_event);
+ return true;
+ end;
+end
+
+local function new(desc)
+ local self = setmetatable({
+ default_state = desc.default_state;
+ events = events.new();
+ }, fsm_mt);
+
+ -- states[state_name][transition_name] = new_state_name
+ local states = { ["*"] = {} };
+ if desc.default_state then
+ states[desc.default_state] = {};
+ end
+ self.states = states;
+
+ local instance_methods = {};
+ self._instance_mt = { __index = instance_methods };
+
+ for _, transition in ipairs(desc.transitions or {}) do
+ local from_states = transition.from;
+ if type(from_states) ~= "table" then
+ from_states = { from_states };
+ end
+ for _, from in ipairs(from_states) do
+ if not states[from] then
+ states[from] = {};
+ end
+ if not states[transition.to] then
+ states[transition.to] = {};
+ end
+ if states[from][transition.name] then
+ return error(("Duplicate transition in FSM specification: %s from %s"):format(transition.name, from));
+ end
+ states[from][transition.name] = transition.to;
+ end
+
+ -- Add public method to trigger this transition
+ instance_methods[transition.name] = do_transition(transition.name);
+ end
+
+ if desc.state_handlers then
+ for state_name, handler in pairs(desc.state_handlers) do
+ self.events.add_handler("enter/"..state_name, handler);
+ end
+ end
+
+ if desc.transition_handlers then
+ for transition_name, handler in pairs(desc.transition_handlers) do
+ self.events.add_handler("transition/"..transition_name, handler);
+ end
+ end
+
+ if desc.handlers then
+ self.events.add_handlers(desc.handlers);
+ end
+
+ return self;
+end
+
+function fsm_methods:init(state_name, state_attr)
+ local initial_state = assert(state_name or self.default_state, "no initial state specified");
+ if not self.states[initial_state] then
+ return error("Invalid initial state: "..initial_state);
+ end
+ local instance = setmetatable({
+ fsm = self;
+ state = initial_state;
+ state_attr = state_attr;
+ }, self._instance_mt);
+
+ if initial_state ~= self.default_state then
+ local fire_event = self.events.fire_event;
+ notify_transitioned(fire_event, {
+ instance = instance;
+
+ to = initial_state;
+ to_attr = state_attr;
+
+ from = self.default_state;
+ });
+ end
+
+ return instance;
+end
+
+function fsm_methods:is_instance(o)
+ local mt = getmetatable(o);
+ return mt == self._instance_mt;
+end
+
+return {
+ new = new;
+ is_fsm = is_fsm;
+};