File

util/fsm.lua @ 13246:2e04d54fb013

mod_tokenauth: Fix revoking a single token without revoking whole grant This appears to have been a copy-paste of the grant revocation function, or maybe the other way around. Either way, it deleted the whole grant instead of the individual token as might be expected.
author Kim Alvefur <zash@zash.se>
date Sun, 23 Jul 2023 02:54:49 +0200
parent 13165:9c13c11b199d
line wrap: on
line source

local events = require "prosody.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;
};