# frozen_string_literal: true require_relative "choice_merger" require_relative "safety" require_relative "transition_builder" module FiniteMachine # A generic DSL for describing the state machine class GenericDSL # Initialize a generic DSL # # @api public def initialize(machine, attrs) @machine = machine @attrs = attrs end # Expose any state constant # @api public def any_state ANY_STATE end # Expose any event constant # @api public def any_event ANY_EVENT end # Delegate attributes to machine instance # # @api private def method_missing(method_name, *args, &block) if @machine.respond_to?(method_name) @machine.send(method_name, *args, &block) else super end end # Check if message can be handled by this DSL # # @api private def respond_to_missing?(method_name, include_private = false) @machine.respond_to?(method_name) || super end # Configure state machine properties # # @api private def call(&block) instance_eval(&block) end end # GenericDSL # A class responsible for adding state machine specific dsl class DSL < GenericDSL include Safety # Initialize top level DSL # # @api public def initialize(machine, attrs) super(machine, attrs) @machine.state = FiniteMachine::DEFAULT_STATE @defer_initial = true @silent_initial = true initial(@attrs[:initial]) if @attrs[:initial] terminal(@attrs[:terminal]) if @attrs[:terminal] log_transitions(@attrs.fetch(:log_transitions, false)) end # Add aliases for the target object # # @example # FiniteMachine.define do # target_alias :engine # # on_transition do |event| # engine.state = event.to # end # end # # @param [Array] aliases # the names for target alias # # @api public def alias_target(*aliases) aliases.each do |alias_name| next if env.aliases.include?(alias_name) env.aliases << alias_name end end # Define initial state # # @param [Symbol] value # The initial state name. # @param [Hash[Symbol]] options # @option options [Symbol] :event # The event name. # @option options [Symbol] :defer # Set to true to defer initial state transition. # Default false. # @option options [Symbol] :silent # Set to true to disable callbacks. # Default true. # # @example # initial :green # # @example Defer initial event # initial state: green, defer: true # # @example Trigger callbacks # initial :green, silent: false # # @example Redefine event name # initial :green, event: :start # # @param [String, Hash] value # # @return [StateMachine] # # @api public def initial(value, options = {}) state = (value && !value.is_a?(Hash)) ? value : raise_missing_state name, @defer_initial, @silent_initial = *parse_initial(options) @initial_event = name event(name, FiniteMachine::DEFAULT_STATE => state, silent: @silent_initial) end # Trigger initial event # # @return [nil] # # @api private def trigger_init method = @silent_initial ? :transition : :trigger @machine.public_send(method, :"#{@initial_event}") unless @defer_initial end # Define terminal state # # @example # terminal :red # # @return [FiniteMachine::StateMachine] # # @api public def terminal(*values) self.terminal_states = values end # Create event and associate transition # # @example # event :go, :green => :yellow # event :go, :green => :yellow, if: :lights_on? # # @param [Symbol] name # the event name # @param [Hash] transitions # the event transitions and conditions # # @return [Transition] # # @api public def event(name, transitions = {}, &block) detect_event_conflict!(name) if machine.auto_methods? if block_given? merger = ChoiceMerger.new(machine, name, transitions) merger.instance_eval(&block) else transition_builder = TransitionBuilder.new(machine, name, transitions) transition_builder.call(transitions) end end # Add error handler # # @param [Array] exceptions # # @example # handle InvalidStateError, with: :log_errors # # @return [Array[Exception]] # # @api public def handle(*exceptions, &block) @machine.handle(*exceptions, &block) end # Decide whether to log transitions # # @api public def log_transitions(value) self.log_transitions = value end private # Parse initial options # # @param [Hash] options # the options to extract for initial state setup # # @return [Array[Symbol,String]] # # @api private def parse_initial(options) [options.fetch(:event) { FiniteMachine::DEFAULT_EVENT_NAME }, options.fetch(:defer) { false }, options.fetch(:silent) { true }] end # Raises missing state error # # @raise [MissingInitialStateError] # Raised when state name is not provided for initial. # # @return [nil] # # @api private def raise_missing_state raise MissingInitialStateError, "Provide state to transition :to for the initial event" end end # DSL end # FiniteMachine