lib/simply_fsm.rb in simply_fsm-0.1.1 vs lib/simply_fsm.rb in simply_fsm-0.1.2

- old
+ new

@@ -1,21 +1,31 @@ # frozen_string_literal: true -require 'simply_fsm/version' +require "simply_fsm/version" +## +# Defines the `SimplyFSM` module module SimplyFSM def self.included(base) base.extend(ClassMethods) end + ## + # Defines the constructor for defining a state machine module ClassMethods + ## + # Declare a state machine called +name+ which can then be defined + # by a DSL defined by the methods of `StateMachine`, with the following +opts+: + # - an optional +fail+ lambda that is called when any event fails to transition) def state_machine(name, opts = {}, &block) fsm = StateMachine.new(name, self, fail: opts[:fail]) fsm.instance_eval(&block) end end + ## + # The DSL for defining a state machine class StateMachine attr_reader :initial_state, :states, :events, :name, :full_name def initialize(name, owner_class, fail: nil) @owner_class = owner_class @@ -27,94 +37,112 @@ @fail_handler = fail setup_base_methods end + ## + # Declare a supported +state_name+, and optionally specify one as the +initial+ state. def state(state_name, initial: false) - unless state_name.nil? || @states.include?(state_name) - status = state_name.to_sym - state_machine_name = @name - @states << status - @initial_state = status if initial + return if state_name.nil? || @states.include?(state_name) - make_owner_method "#{state_name}?", lambda { - send(state_machine_name) == status - } - end + status = state_name.to_sym + state_machine_name = @name + @states << status + @initial_state = status if initial + + make_owner_method "#{state_name}?", lambda { + send(state_machine_name) == status + } end + ## + # Define an event by +event_name+ and + # - its +transition+ as a hash with a +from+ state or array of states and the +to+ state, + # - an optional +guard+ lambda which must return true for the transition to occur, + # - an optional +fail+ lambda that is called when the transition fails (overrides top-level fail handler), and + # - an optional do block that is called +after+ the transition succeeds def event(event_name, transition:, guard: nil, fail: nil, &after) - if event_name && transition - @events << event_name - from = transition[:from] - to = transition[:to] - state_machine_name = @name - var_name = "@#{state_machine_name}" - may_event_name = "may_#{event_name}?" - fail = @fail_handler if fail.nil? + return unless event_exists?(event_name) && transition - setup_may_event_method may_event_name, from, to, guard + @events << event_name + to = transition[:to] + may_event_name = "may_#{event_name}?" - # - # Setup the event method to attempt to make the state - # transition or report failure - make_owner_method event_name, lambda { - if send(may_event_name) - instance_variable_set(var_name, to) - instance_exec(&after) if after - return true - end - # unable to satisfy pre-conditions for the event - if fail - if fail.is_a?(String) || fail.is_a?(Symbol) - send(fail, event_name) - else - instance_exec(event_name, &fail) - end - end - false - } - - end + setup_may_event_method may_event_name, transition[:from], to, guard + setup_event_method event_name, var_name: "@#{@name}", + may_event_name: may_event_name, to: to, + fail: fail || @fail_handler, &after end private + def event_exists?(event_name) + event_name && !@events.include?(event_name) + end + + def setup_event_method(event_name, var_name:, may_event_name:, to:, fail:, &after) + method_lambda = lambda { + if send(may_event_name) + instance_variable_set(var_name, to) + instance_exec(&after) if after + return true + end + # unable to satisfy pre-conditions for the event + if fail + if fail.is_a?(String) || fail.is_a?(Symbol) + send(fail, event_name) + else + instance_exec(event_name, &fail) + end + end + false + } + make_owner_method event_name, method_lambda + end + def setup_may_event_method(may_event_name, from, _to, guard) state_machine_name = @name # - # Instead of one "may_event?" method that checks all variations - # every time it's called, here we check the event definition and - # define the most optimal lambda to ensure the check is as fast as - # possible + # Instead of one "may_event?" method that checks all variations every time it's called, here we check + # the event definition and define the most optimal lambda to ensure the check is as fast as possible method_lambda = if from == :any && !guard -> { true } # unguarded transition from any state elsif from == :any - guard # guarded transition from any state + guard # guarded transition from any state elsif !guard - if from.is_a?(Array) - lambda { # unguarded transition from choice of states - current = send(state_machine_name) - from.include?(current) - } - else - lambda { # unguarded transition from one state - current = send(state_machine_name) - from == current - } - end - elsif from.is_a?(Array) - lambda { # guarded transition from choice of states - current = send(state_machine_name) - from.include?(current) && instance_exec(&guard) - } + guardless_may_event_lambda(from, state_machine_name) else - lambda { # guarded transition from one state - current = send(state_machine_name) - from == current && instance_exec(&guard) - } + guarded_may_event_lambda(from, guard, state_machine_name) end make_owner_method may_event_name, method_lambda + end + + def guarded_may_event_lambda(from, guard, state_machine_name) + if from.is_a?(Array) + lambda { # guarded transition from choice of states + current = send(state_machine_name) + from.include?(current) && instance_exec(&guard) + } + else + lambda { # guarded transition from one state + current = send(state_machine_name) + from == current && instance_exec(&guard) + } + end + end + + def guardless_may_event_lambda(from, state_machine_name) + if from.is_a?(Array) + lambda { # unguarded transition from choice of states + current = send(state_machine_name) + from.include?(current) + } + else + lambda { # unguarded transition from one state + current = send(state_machine_name) + from == current + } + end end def setup_base_methods var_name = "@#{name}" fsm = self