lib/simply_fsm.rb in simply_fsm-0.2.3 vs lib/simply_fsm.rb in simply_fsm-0.3.0
- old
+ new
@@ -1,280 +1,4 @@
# frozen_string_literal: true
require "simply_fsm/version"
-
-#
-# Include *SimplyFSM* in a class to be able to defined state machines.
-#
-module SimplyFSM
- #
- # Provides a +state_machine+ for the including class.
- 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*.
- #
- # @param [String] name of the state machine.
- # @param [Hash] opts to specify options such as:
- # - +fail+ lambda that is called with the event name 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. These methods are used within the declaration of a +state_machine+.
- #
- # @attr_reader [String] initial_state The initial state of the state machine
- # @attr_reader [Array] states All the states of the state machine
- # @attr_reader [Array] events All the events of the state machine
- # @attr_reader [String] name
- # @attr_reader [String] full_name The name of the owning class combined with the state machine's name
- #
- class StateMachine
- attr_reader :initial_state, :states, :events, :name, :full_name
-
- #
- # @!visibility private
- def initialize(name, owner_class, fail: nil)
- @owner_class = owner_class
- @name = name.to_sym
- @full_name = "#{owner_class.name}/#{name}"
- @states = []
- @events = []
- @initial_state = nil
- @fail_handler = fail
-
- setup_base_methods
- end
-
- #
- # Declare a supported +state_name+, and optionally specify one as the +initial+ state.
- #
- # @param [String] state_name
- # @param [Boolean] initial to indicate if this is the initial state of the state machine
- #
- def state(state_name, initial: false)
- return if state_name.nil? || @states.include?(state_name)
-
- 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+
- #
- # @param [String] event_name
- # @param [Hash,Array] transitions either one (Hash) or many (Array of Hashes) transitions +from+ one state +to+ another state.
- # @param [Lambda] guard if specified must return +true+ before any transitions are attempted
- # @param [Lambda] fail called with event name if specified when all the attempted transitions fail
- # @yield when the transition attempt succeeds.
- def event(event_name, transitions:, guard: nil, fail: nil, &after)
- return unless event_exists?(event_name) && transitions
-
- @events << event_name
- may_event_name = "may_#{event_name}?"
-
- if transitions.is_a?(Array)
- setup_multi_transition_may_event_method transitions: transitions, guard: guard,
- may_event_name: may_event_name
- setup_multi_transition_event_method event_name,
- transitions: transitions, guard: guard,
- var_name: "@#{@name}", fail: fail || @fail_handler
- return
- end
-
- to = transitions[:to]
- setup_may_event_method may_event_name, transitions[:from] || :any, transitions[:when], 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 setup_multi_transition_may_event_method(transitions:, guard:, may_event_name:)
- state_machine_name = @name
-
- make_owner_method may_event_name, lambda {
- if !guard || instance_exec(&guard)
- current = send(state_machine_name)
- # Check each transition, and first one that succeeds ends the scan
- transitions.each do |t|
- next if cannot_transition?(t[:from], t[:when], current)
-
- return true
- end
- end
- false
- }
- end
-
- def setup_fail_lambda_for(fail)
- return unless fail
-
- if fail.is_a?(String) || fail.is_a?(Symbol)
- ->(event_name) { send(fail, event_name) }
- else
- fail
- end
- end
-
- def setup_multi_transition_event_method(event_name, transitions:, guard:, var_name:, fail:)
- state_machine_name = @name
- fail_lambda = setup_fail_lambda_for(fail)
- make_owner_method event_name, lambda {
- if !guard || instance_exec(&guard)
- current = send(state_machine_name)
- # Check each transition, and first one that succeeds ends the scan
- transitions.each do |t|
- next if cannot_transition?(t[:from], t[:when], current)
-
- instance_variable_set(var_name, t[:to])
- return true
- end
- end
- instance_exec(event_name, &fail_lambda) if fail_lambda
- false
- }
- end
-
- 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)
- fail_lambda = setup_fail_lambda_for(fail)
- method_lambda = lambda {
- if send(may_event_name)
- instance_variable_set(var_name, to)
- instance_exec(&after) if after
- return true
- end
- instance_exec(event_name, &fail_lambda) if fail_lambda
- false
- }
- make_owner_method event_name, method_lambda
- end
-
- def setup_may_event_method(may_event_name, from, cond, 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
- method_lambda = if from == :any
- from_any_may_event_lambda(guard, cond, state_machine_name)
- else
- guarded_or_conditional_may_event_lambda(from, guard, cond, state_machine_name)
- end
- make_owner_method may_event_name, method_lambda
- end
-
- def from_any_may_event_lambda(guard, cond, _state_machine_name)
- if !guard && !cond
- -> { true } # unguarded transition from any state
- elsif !cond
- guard # guarded transition from any state
- elsif !guard
- cond # conditional unguarded transition from any state
- else
- -> { instance_exec(&guard) && instance_exec(&cond) }
- end
- end
-
- def guarded_or_conditional_may_event_lambda(from, guard, cond, state_machine_name)
- if !guard && !cond
- guardless_may_event_lambda(from, state_machine_name)
- elsif !cond
- guarded_may_event_lambda(from, guard, state_machine_name)
- elsif !guard
- guarded_may_event_lambda(from, cond, state_machine_name)
- else
- guarded_and_conditional_may_event_lambda(from, guard, cond, state_machine_name)
- end
- 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 guarded_and_conditional_may_event_lambda(from, guard, cond, 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) && instance_exec(&cond)
- }
- else
- lambda { # guarded transition from one state
- current = send(state_machine_name)
- from == current && instance_exec(&guard) && instance_exec(&cond)
- }
- 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
- make_owner_method @name, lambda {
- instance_variable_get(var_name) ||
- fsm.initial_state
- }
- make_owner_method "#{@name}_states", -> { fsm.states }
- make_owner_method "#{@name}_events", -> { fsm.events }
- end
-
- def make_owner_method(method_name, method_definition)
- @owner_class.define_method(method_name, method_definition)
- end
- end
-
- private
-
- def state_match?(from, current)
- return true if from == :any
- return from.include?(current) if from.is_a?(Array)
-
- from == current
- end
-
- def cannot_transition?(from, cond, current)
- (from && !state_match?(from, current)) || (cond && !instance_exec(&cond))
- end
-end
+require "simply_fsm/simply_fsm"