Class: StateMachines::Event

Inherits:
Object
  • Object
show all
Includes:
MatcherHelpers
Defined in:
lib/state_machines/event.rb

Overview

An event defines an action that transitions an attribute from one state to another. The state that an attribute is transitioned to depends on the branches configured for the event.

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from MatcherHelpers

#all, #same

Constructor Details

#initialize(machine, name, options = nil, human_name: nil, **extra_options) ⇒ Event

Creates a new event within the context of the given machine

Configuration options:

  • :human_name - The human-readable version of this event’s name



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/state_machines/event.rb', line 36

def initialize(machine, name, options = nil, human_name: nil, **extra_options) # :nodoc:
  # Handle both old hash style and new kwargs style for backward compatibility
  case options
  in Hash
    # Old style: initialize(machine, name, {human_name: 'Custom Name'})
    StateMachines::OptionsValidator.assert_valid_keys!(options, :human_name)
    human_name = options[:human_name]
  in nil
    # New style: initialize(machine, name, human_name: 'Custom Name')
    StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :human_name) unless extra_options.empty?
  else
    # Handle unexpected options
    raise ArgumentError, "Unexpected positional argument in Event initialize: #{options.inspect}"
  end

  @machine = machine
  @name = name
  @qualified_name = machine.namespace ? :"#{name}_#{machine.namespace}" : name
  @human_name = human_name || @name.to_s.tr('_', ' ')
  reset

  # Output a warning if another event has a conflicting qualified name
  if (conflict = machine.owner_class.state_machines.detect { |_other_name, other_machine| other_machine != @machine && other_machine.events[qualified_name, :qualified_name] })
    _name, other_machine = conflict
    warn "Event #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
  else
    add_actions
  end
end

Instance Attribute Details

#branchesObject (readonly)

The list of branches that determine what state this event transitions objects to when fired



26
27
28
# File 'lib/state_machines/event.rb', line 26

def branches
  @branches
end

#human_name(klass = @machine.owner_class) ⇒ Object

Transforms the event name into a more human-readable format, such as “turn on” instead of “turn_on”



76
77
78
# File 'lib/state_machines/event.rb', line 76

def human_name(klass = @machine.owner_class)
  @human_name.is_a?(Proc) ? @human_name.call(self, klass) : @human_name
end

#known_statesObject (readonly)

A list of all of the states known to this event using the configured branches/transitions as the source



30
31
32
# File 'lib/state_machines/event.rb', line 30

def known_states
  @known_states
end

#machineObject

The state machine for which this event is defined



13
14
15
# File 'lib/state_machines/event.rb', line 13

def machine
  @machine
end

#nameObject (readonly)

The name of the event



16
17
18
# File 'lib/state_machines/event.rb', line 16

def name
  @name
end

#qualified_nameObject (readonly)

The fully-qualified name of the event, scoped by the machine’s namespace



19
20
21
# File 'lib/state_machines/event.rb', line 19

def qualified_name
  @qualified_name
end

Instance Method Details

#can_fire?(object, requirements = {}) ⇒ Boolean

Determines whether any transitions can be performed for this event based on the current state of the given object.

If the event can’t be fired, then this will return false, otherwise true.

Note that this will not take the object context into account. Although a transition may be possible based on the state machine definition, object-specific behaviors (like validations) may prevent it from firing.

Returns:

  • (Boolean)


122
123
124
# File 'lib/state_machines/event.rb', line 122

def can_fire?(object, requirements = {})
  !transition_for(object, requirements).nil?
end

#contextObject

Evaluates the given block within the context of this event. This simply provides a DSL-like syntax for defining transitions.



82
83
84
# File 'lib/state_machines/event.rb', line 82

def context(&)
  instance_eval(&)
end

#draw(graph, options = {}, io = $stdout) ⇒ Object



199
200
201
# File 'lib/state_machines/event.rb', line 199

def draw(graph, options = {}, io = $stdout)
  machine.renderer.draw_event(self, graph, options, io)
end

#fire(object, *event_args) ⇒ Object

Attempts to perform the next available transition on the given object. If no transitions can be made, then this will return false, otherwise true.

Any additional arguments are passed to the StateMachines::Transition#perform instance method.



168
169
170
171
172
173
174
175
176
177
# File 'lib/state_machines/event.rb', line 168

def fire(object, *event_args)
  machine.reset(object)

  if (transition = transition_for(object, {}, *event_args))
    transition.perform(*event_args)
  else
    on_failure(object, *event_args)
    false
  end
end

#initialize_copy(orig) ⇒ Object

Creates a copy of this event in addition to the list of associated branches to prevent conflicts across events within a class hierarchy.



68
69
70
71
72
# File 'lib/state_machines/event.rb', line 68

def initialize_copy(orig) # :nodoc:
  super
  @branches = @branches.dup
  @known_states = @known_states.dup
end

#inspectObject

Generates a nicely formatted description of this event’s contents.

For example,

event = StateMachines::Event.new(machine, :park)
event.transition all - :idling => :parked, :idling => same
event   # => #<StateMachines::Event name=:park transitions=[all - :idling => :parked, :idling => same]>


210
211
212
213
214
215
216
217
218
# File 'lib/state_machines/event.rb', line 210

def inspect
  transitions = branches.flat_map do |branch|
    branch.state_requirements.map do |state_requirement|
      "#{state_requirement[:from].description} => #{state_requirement[:to].description}"
    end
  end.join(', ')

  "#<#{self.class} name=#{name.inspect} transitions=[#{transitions}]>"
end

#on_failure(object, *args) ⇒ Object

Marks the object as invalid and runs any failure callbacks associated with this event. This should get called anytime this event fails to transition.



181
182
183
184
185
186
187
188
# File 'lib/state_machines/event.rb', line 181

def on_failure(object, *args)
  state = machine.states.match!(object)
  machine.invalidate(object, :state, :invalid_transition, [[:event, human_name(object.class)], [:state, state.human_name(object.class)]])

  transition = Transition.new(object, machine, name, state.name, state.name)
  transition.args = args if args.any?
  transition.run_callbacks(before: false)
end

#resetObject

Resets back to the initial state of the event, with no branches / known states associated. This allows you to redefine an event in situations where you either are re-using an existing state machine implementation or are subclassing machines.



194
195
196
197
# File 'lib/state_machines/event.rb', line 194

def reset
  @branches = []
  @known_states = []
end

#transition(options) ⇒ Object

Creates a new transition that determines what to change the current state to when this event fires.

Since this transition is being defined within an event context, you do not need to specify the :on option for the transition. For example:

state_machine do
  event :ignite do
    transition :parked => :idling, :idling => same, :if => :seatbelt_on? # Transitions to :idling if seatbelt is on
    transition all => :parked, :unless => :seatbelt_on?                  # Transitions to :parked if seatbelt is off
  end
end

See StateMachines::Machine#transition for a description of the possible configurations for defining transitions.

Raises:

  • (ArgumentError)


102
103
104
105
106
107
108
109
110
111
112
# File 'lib/state_machines/event.rb', line 102

def transition(options)
  raise ArgumentError, 'Must specify as least one transition requirement' if options.empty?

  # Only a certain subset of explicit options are allowed for transition
  # requirements
  StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :except_from, :except_to, :if, :unless, :if_state, :unless_state, :if_all_states, :unless_all_states, :if_any_state, :unless_any_state) if (options.keys - %i[from to on except_from except_to except_on if unless if_state unless_state if_all_states unless_all_states if_any_state unless_any_state]).empty?

  branches << branch = Branch.new(options.merge(on: name))
  @known_states |= branch.known_states
  branch
end

#transition_for(object, requirements = {}, *event_args) ⇒ Object

Finds and builds the next transition that can be performed on the given object. If no transitions can be made, then this will return nil.

Valid requirement options:

  • :from - One or more states being transitioned from. If none are specified, then this will be the object’s current state.

  • :to - One or more states being transitioned to. If none are specified, then this will match any to state.

  • :guard - Whether to guard transitions with the if/unless conditionals defined for each one. Default is true.

Event arguments are passed to guard conditions if they accept multiple parameters.



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/state_machines/event.rb', line 138

def transition_for(object, requirements = {}, *event_args)
  StateMachines::OptionsValidator.assert_valid_keys!(requirements, :from, :to, :guard)
  requirements[:from] = machine.states.match!(object).name unless (custom_from_state = requirements.include?(:from))

  branches.each do |branch|
    next unless (match = branch.match(object, requirements, event_args))

    # Branch allows for the transition to occur
    from = requirements[:from]
    to = if match[:to].is_a?(LoopbackMatcher)
           from
         else
           values = requirements.include?(:to) ? [requirements[:to]].flatten : [from] | machine.states.map { |state| state.name }

           match[:to].filter(values).first
         end

    return Transition.new(object, machine, name, from, to, !custom_from_state)
  end

  # No transition matched
  nil
end