state_machines 0.20.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +124 -13
  3. data/lib/state_machines/branch.rb +12 -13
  4. data/lib/state_machines/callback.rb +11 -12
  5. data/lib/state_machines/core.rb +0 -1
  6. data/lib/state_machines/error.rb +5 -4
  7. data/lib/state_machines/eval_helpers.rb +83 -45
  8. data/lib/state_machines/event.rb +23 -26
  9. data/lib/state_machines/event_collection.rb +4 -5
  10. data/lib/state_machines/extensions.rb +5 -5
  11. data/lib/state_machines/helper_module.rb +1 -1
  12. data/lib/state_machines/integrations/base.rb +1 -1
  13. data/lib/state_machines/integrations.rb +11 -14
  14. data/lib/state_machines/machine/action_hooks.rb +53 -0
  15. data/lib/state_machines/machine/callbacks.rb +59 -0
  16. data/lib/state_machines/machine/class_methods.rb +25 -11
  17. data/lib/state_machines/machine/configuration.rb +124 -0
  18. data/lib/state_machines/machine/event_methods.rb +59 -0
  19. data/lib/state_machines/machine/helper_generators.rb +125 -0
  20. data/lib/state_machines/machine/integration.rb +70 -0
  21. data/lib/state_machines/machine/parsing.rb +77 -0
  22. data/lib/state_machines/machine/rendering.rb +17 -0
  23. data/lib/state_machines/machine/scoping.rb +44 -0
  24. data/lib/state_machines/machine/state_methods.rb +101 -0
  25. data/lib/state_machines/machine/utilities.rb +85 -0
  26. data/lib/state_machines/machine/validation.rb +39 -0
  27. data/lib/state_machines/machine.rb +73 -617
  28. data/lib/state_machines/machine_collection.rb +18 -14
  29. data/lib/state_machines/macro_methods.rb +2 -2
  30. data/lib/state_machines/matcher.rb +6 -6
  31. data/lib/state_machines/matcher_helpers.rb +1 -1
  32. data/lib/state_machines/node_collection.rb +18 -17
  33. data/lib/state_machines/path.rb +2 -4
  34. data/lib/state_machines/path_collection.rb +2 -3
  35. data/lib/state_machines/state.rb +6 -5
  36. data/lib/state_machines/state_collection.rb +3 -3
  37. data/lib/state_machines/state_context.rb +6 -7
  38. data/lib/state_machines/stdio_renderer.rb +16 -16
  39. data/lib/state_machines/syntax_validator.rb +57 -0
  40. data/lib/state_machines/test_helper.rb +290 -27
  41. data/lib/state_machines/transition.rb +43 -41
  42. data/lib/state_machines/transition_collection.rb +22 -25
  43. data/lib/state_machines/version.rb +1 -1
  44. metadata +23 -9
@@ -7,7 +7,6 @@ module StateMachines
7
7
  # another. The state that an attribute is transitioned to depends on the
8
8
  # branches configured for the event.
9
9
  class Event
10
-
11
10
  include MatcherHelpers
12
11
 
13
12
  # The state machine for which this event is defined
@@ -34,7 +33,7 @@ module StateMachines
34
33
  #
35
34
  # Configuration options:
36
35
  # * <tt>:human_name</tt> - The human-readable version of this event's name
37
- def initialize(machine, name, options = nil, human_name: nil, **extra_options) #:nodoc:
36
+ def initialize(machine, name, options = nil, human_name: nil, **extra_options) # :nodoc:
38
37
  # Handle both old hash style and new kwargs style for backward compatibility
39
38
  if options.is_a?(Hash)
40
39
  # Old style: initialize(machine, name, {human_name: 'Custom Name'})
@@ -43,6 +42,7 @@ module StateMachines
43
42
  else
44
43
  # New style: initialize(machine, name, human_name: 'Custom Name')
45
44
  raise ArgumentError, "Unexpected positional argument: #{options.inspect}" unless options.nil?
45
+
46
46
  StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :human_name) unless extra_options.empty?
47
47
  end
48
48
 
@@ -63,7 +63,7 @@ module StateMachines
63
63
 
64
64
  # Creates a copy of this event in addition to the list of associated
65
65
  # branches to prevent conflicts across events within a class hierarchy.
66
- def initialize_copy(orig) #:nodoc:
66
+ def initialize_copy(orig) # :nodoc:
67
67
  super
68
68
  @branches = @branches.dup
69
69
  @known_states = @known_states.dup
@@ -77,8 +77,8 @@ module StateMachines
77
77
 
78
78
  # Evaluates the given block within the context of this event. This simply
79
79
  # provides a DSL-like syntax for defining transitions.
80
- def context(&block)
81
- instance_eval(&block)
80
+ def context(&)
81
+ instance_eval(&)
82
82
  end
83
83
 
84
84
  # Creates a new transition that determines what to change the current state
@@ -102,9 +102,7 @@ module StateMachines
102
102
 
103
103
  # Only a certain subset of explicit options are allowed for transition
104
104
  # requirements
105
- if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).empty?
106
- StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :except_from, :except_to, :if, :unless)
107
- end
105
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :except_from, :except_to, :if, :unless) if (options.keys - %i[from to on except_from except_to except_on if unless]).empty?
108
106
 
109
107
  branches << branch = Branch.new(options.merge(on: name))
110
108
  @known_states |= branch.known_states
@@ -138,19 +136,19 @@ module StateMachines
138
136
  requirements[:from] = machine.states.match!(object).name unless (custom_from_state = requirements.include?(:from))
139
137
 
140
138
  branches.each do |branch|
141
- if (match = branch.match(object, requirements))
142
- # Branch allows for the transition to occur
143
- from = requirements[:from]
144
- to = if match[:to].is_a?(LoopbackMatcher)
145
- from
146
- else
147
- values = requirements.include?(:to) ? [requirements[:to]].flatten : [from] | machine.states.map { |state| state.name }
148
-
149
- match[:to].filter(values).first
150
- end
151
-
152
- return Transition.new(object, machine, name, from, to, !custom_from_state)
153
- end
139
+ next unless (match = branch.match(object, requirements))
140
+
141
+ # Branch allows for the transition to occur
142
+ from = requirements[:from]
143
+ to = if match[:to].is_a?(LoopbackMatcher)
144
+ from
145
+ else
146
+ values = requirements.include?(:to) ? [requirements[:to]].flatten : [from] | machine.states.map { |state| state.name }
147
+
148
+ match[:to].filter(values).first
149
+ end
150
+
151
+ return Transition.new(object, machine, name, from, to, !custom_from_state)
154
152
  end
155
153
 
156
154
  # No transition matched
@@ -163,13 +161,13 @@ module StateMachines
163
161
  #
164
162
  # Any additional arguments are passed to the StateMachines::Transition#perform
165
163
  # instance method.
166
- def fire(object, *args)
164
+ def fire(object, *)
167
165
  machine.reset(object)
168
166
 
169
167
  if (transition = transition_for(object))
170
- transition.perform(*args)
168
+ transition.perform(*)
171
169
  else
172
- on_failure(object, *args)
170
+ on_failure(object, *)
173
171
  false
174
172
  end
175
173
  end
@@ -194,7 +192,6 @@ module StateMachines
194
192
  @known_states = []
195
193
  end
196
194
 
197
-
198
195
  def draw(graph, options = {}, io = $stdout)
199
196
  machine.renderer.draw_event(self, graph, options, io)
200
197
  end
@@ -216,7 +213,7 @@ module StateMachines
216
213
  "#<#{self.class} name=#{name.inspect} transitions=[#{transitions * ', '}]>"
217
214
  end
218
215
 
219
- protected
216
+ protected
220
217
 
221
218
  # Add the various instance methods that can transition the object using
222
219
  # the current event
@@ -3,8 +3,8 @@
3
3
  module StateMachines
4
4
  # Represents a collection of events in a state machine
5
5
  class EventCollection < NodeCollection
6
- def initialize(machine) #:nodoc:
7
- super(machine, index: [:name, :qualified_name])
6
+ def initialize(machine) # :nodoc:
7
+ super(machine, index: %i[name qualified_name])
8
8
  end
9
9
 
10
10
  # Gets the list of events that can be fired on the given object.
@@ -130,12 +130,11 @@ module StateMachines
130
130
  false
131
131
  end
132
132
  end
133
-
134
133
  end
135
134
 
136
- private
135
+ private
137
136
 
138
- def match(requirements) #:nodoc:
137
+ def match(requirements) # :nodoc:
139
138
  requirements && requirements[:on] ? [fetch(requirements.delete(:on))] : self
140
139
  end
141
140
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module StateMachines
4
4
  module ClassMethods
5
- def self.extended(base) #:nodoc:
5
+ def self.extended(base) # :nodoc:
6
6
  base.class_eval do
7
7
  @state_machines = MachineCollection.new
8
8
  end
@@ -138,13 +138,13 @@ module StateMachines
138
138
  # vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachines::InvalidParallelTransition: Cannot run events in parallel: ignite, disable_alarm
139
139
  def fire_events!(*events)
140
140
  run_action = [true, false].include?(events.last) ? events.pop : true
141
- fire_events(*(events + [run_action])) || fail(StateMachines::InvalidParallelTransition.new(self, events))
141
+ fire_events(*(events + [run_action])) || raise(StateMachines::InvalidParallelTransition.new(self, events))
142
142
  end
143
143
 
144
- protected
144
+ protected
145
145
 
146
- def initialize_state_machines(options = {}, &block) #:nodoc:
147
- self.class.state_machines.initialize_states(self, options, &block)
146
+ def initialize_state_machines(options = {}, &) # :nodoc:
147
+ self.class.state_machines.initialize_states(self, options, &)
148
148
  end
149
149
  end
150
150
  end
@@ -3,7 +3,7 @@
3
3
  module StateMachines
4
4
  # Represents a type of module that defines instance / class methods for a
5
5
  # state machine
6
- class HelperModule < Module #:nodoc:
6
+ class HelperModule < Module # :nodoc:
7
7
  def initialize(machine, kind)
8
8
  @machine = machine
9
9
  @kind = kind
@@ -35,7 +35,7 @@ module StateMachines
35
35
  end
36
36
  end
37
37
 
38
- def self.included(base) #:nodoc:
38
+ def self.included(base) # :nodoc:
39
39
  base.extend ClassMethods
40
40
  end
41
41
  end
@@ -26,15 +26,15 @@ module StateMachines
26
26
  # Register integration
27
27
  def register(name_or_module)
28
28
  case name_or_module.class.to_s
29
- when 'Module'
30
- add(name_or_module)
31
- else
32
- fail IntegrationError
29
+ when 'Module'
30
+ add(name_or_module)
31
+ else
32
+ raise IntegrationError
33
33
  end
34
34
  true
35
35
  end
36
36
 
37
- def reset #:nodoc:#
37
+ def reset # :nodoc:#
38
38
  @integrations = []
39
39
  end
40
40
 
@@ -47,12 +47,9 @@ module StateMachines
47
47
  # StateMachines::Integrations.register(StateMachines::Integrations::ActiveModel)
48
48
  # StateMachines::Integrations.integrations
49
49
  # # => [StateMachines::Integrations::ActiveModel]
50
- def integrations
51
- # Register all namespaced integrations
52
- @integrations
53
- end
50
+ attr_reader :integrations
54
51
 
55
- alias_method :list, :integrations
52
+ alias list integrations
56
53
 
57
54
  # Attempts to find an integration that matches the given class. This will
58
55
  # look through all of the built-in integrations under the StateMachines::Integrations
@@ -102,12 +99,12 @@ module StateMachines
102
99
  integrations.detect { |integration| integration.integration_name == name } || raise(IntegrationNotFound.new(name))
103
100
  end
104
101
 
105
- private
102
+ private
106
103
 
107
104
  def add(integration)
108
- if integration.respond_to?(:integration_name)
109
- @integrations.insert(0, integration) unless @integrations.include?(integration)
110
- end
105
+ return unless integration.respond_to?(:integration_name)
106
+
107
+ @integrations.insert(0, integration) unless @integrations.include?(integration)
111
108
  end
112
109
  end
113
110
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module ActionHooks
6
+ protected
7
+
8
+ # Determines whether action helpers should be defined for this machine.
9
+ # This is only true if there is an action configured and no other machines
10
+ # have process this same configuration already.
11
+ def define_action_helpers?
12
+ action && owner_class.state_machines.none? { |_name, machine| machine.action == action && machine != self }
13
+ end
14
+
15
+ # Adds helper methods for automatically firing events when an action
16
+ # is invoked
17
+ def define_action_helpers
18
+ return unless action_hook
19
+
20
+ @action_hook_defined = true
21
+ define_action_hook
22
+ end
23
+
24
+ # Hooks directly into actions by defining the same method in an included
25
+ # module. As a result, when the action gets invoked, any state events
26
+ # defined for the object will get run. Method visibility is preserved.
27
+ def define_action_hook
28
+ action_hook = self.action_hook
29
+ action = self.action
30
+ private_action_hook = owner_class.private_method_defined?(action_hook)
31
+
32
+ # Only define helper if it hasn't
33
+ define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
34
+ def #{action_hook}(*)
35
+ self.class.state_machines.transitions(self, #{action.inspect}).perform { super }
36
+ end
37
+
38
+ private #{action_hook.inspect} if #{private_action_hook}
39
+ END_EVAL
40
+ end
41
+
42
+ # The method to hook into for triggering transitions when invoked. By
43
+ # default, this is the action configured for the machine.
44
+ #
45
+ # Since the default hook technique relies on module inheritance, the
46
+ # action must be defined in an ancestor of the owner classs in order for
47
+ # it to be the action hook.
48
+ def action_hook
49
+ action && owner_class_ancestor_has_method?(:instance, action) ? action : nil
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module Callbacks
6
+ # Creates a callback that will be invoked *before* a transition is
7
+ # performed so long as the given requirements match the transition.
8
+ def before_transition(*args, **options, &)
9
+ # Extract legacy positional arguments and merge with keyword options
10
+ parsed_options = parse_callback_arguments(args, options)
11
+
12
+ # Only validate callback-specific options, not state transition requirements
13
+ callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
14
+ StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
15
+
16
+ add_callback(:before, parsed_options, &)
17
+ end
18
+
19
+ # Creates a callback that will be invoked *after* a transition is
20
+ # performed so long as the given requirements match the transition.
21
+ def after_transition(*args, **options, &)
22
+ # Extract legacy positional arguments and merge with keyword options
23
+ parsed_options = parse_callback_arguments(args, options)
24
+
25
+ # Only validate callback-specific options, not state transition requirements
26
+ callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
27
+ StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
28
+
29
+ add_callback(:after, parsed_options, &)
30
+ end
31
+
32
+ # Creates a callback that will be invoked *around* a transition so long
33
+ # as the given requirements match the transition.
34
+ def around_transition(*args, **options, &)
35
+ # Extract legacy positional arguments and merge with keyword options
36
+ parsed_options = parse_callback_arguments(args, options)
37
+
38
+ # Only validate callback-specific options, not state transition requirements
39
+ callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
40
+ StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
41
+
42
+ add_callback(:around, parsed_options, &)
43
+ end
44
+
45
+ # Creates a callback that will be invoked after a transition has failed
46
+ # to be performed.
47
+ def after_failure(*args, **options, &)
48
+ # Extract legacy positional arguments and merge with keyword options
49
+ parsed_options = parse_callback_arguments(args, options)
50
+
51
+ # Only validate callback-specific options, not state transition requirements
52
+ callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
53
+ StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
54
+
55
+ add_callback(:failure, parsed_options, &)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -14,14 +14,14 @@ module StateMachines
14
14
  # If a machine of the given name already exists in one of the class's
15
15
  # superclasses, then a copy of that machine will be created and stored
16
16
  # in the new owner class (the original will remain unchanged).
17
- def find_or_create(owner_class, *args, &block)
17
+ def find_or_create(owner_class, *args, &)
18
18
  options = args.last.is_a?(Hash) ? args.pop : {}
19
19
  name = args.first || :state
20
20
 
21
21
  # Find an existing machine
22
- machine = owner_class.respond_to?(:state_machines) &&
23
- (args.first && owner_class.state_machines[name] || !args.first &&
24
- owner_class.state_machines.values.first) || nil
22
+ machine = (owner_class.respond_to?(:state_machines) &&
23
+ ((args.first && owner_class.state_machines[name]) || (!args.first &&
24
+ owner_class.state_machines.values.first))) || nil
25
25
 
26
26
  if machine
27
27
  # Only create a new copy if changes are being made to the machine in
@@ -33,10 +33,10 @@ module StateMachines
33
33
  end
34
34
 
35
35
  # Evaluate DSL
36
- machine.instance_eval(&block) if block_given?
36
+ machine.instance_eval(&) if block_given?
37
37
  else
38
38
  # No existing machine: create a new one
39
- machine = new(owner_class, name, options, &block)
39
+ machine = new(owner_class, name, options, &)
40
40
  end
41
41
 
42
42
  machine
@@ -47,6 +47,7 @@ module StateMachines
47
47
  end
48
48
 
49
49
  # Default messages to use for validation errors in ORM integrations
50
+ # Thread-safe access via atomic operations on simple values
50
51
  attr_accessor :ignore_method_conflicts
51
52
 
52
53
  def default_messages
@@ -54,17 +55,19 @@ module StateMachines
54
55
  invalid: 'is invalid',
55
56
  invalid_event: 'cannot transition when %s',
56
57
  invalid_transition: 'cannot transition via "%1$s"'
57
- }
58
+ }.freeze
58
59
  end
59
60
 
60
61
  def default_messages=(messages)
61
- @default_messages = messages
62
+ # Atomic replacement with frozen object
63
+ @default_messages = deep_freeze_hash(messages)
62
64
  end
63
65
 
64
66
  def replace_messages(message_hash)
65
- message_hash.each do |key, value|
66
- default_messages[key] = value
67
- end
67
+ # Atomic replacement: read current messages, merge with new ones, replace atomically
68
+ current_messages = @default_messages || {}
69
+ merged_messages = current_messages.merge(message_hash)
70
+ @default_messages = deep_freeze_hash(merged_messages)
68
71
  end
69
72
 
70
73
  attr_writer :renderer
@@ -74,6 +77,17 @@ module StateMachines
74
77
 
75
78
  STDIORenderer
76
79
  end
80
+
81
+ private
82
+
83
+ # Deep freezes a hash and all its string values for thread safety
84
+ def deep_freeze_hash(hash)
85
+ hash.each_with_object({}) do |(key, value), frozen_hash|
86
+ frozen_key = key.respond_to?(:freeze) ? key.freeze : key
87
+ frozen_value = value.respond_to?(:freeze) ? value.freeze : value
88
+ frozen_hash[frozen_key] = frozen_value
89
+ end.freeze
90
+ end
77
91
  end
78
92
  end
79
93
  end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module Configuration
6
+ # Initializes a new state machine with the given configuration.
7
+ def initialize(owner_class, *args, &)
8
+ options = args.last.is_a?(Hash) ? args.pop : {}
9
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
10
+
11
+ # Find an integration that matches this machine's owner class
12
+ @integration = if options.include?(:integration)
13
+ options[:integration] && StateMachines::Integrations.find_by_name(options[:integration])
14
+ else
15
+ StateMachines::Integrations.match(owner_class)
16
+ end
17
+
18
+ if @integration
19
+ extend @integration
20
+ options = (@integration.defaults || {}).merge(options)
21
+ end
22
+
23
+ # Add machine-wide defaults
24
+ options = { use_transactions: true, initialize: true }.merge(options)
25
+
26
+ # Set machine configuration
27
+ @name = args.first || :state
28
+ @attribute = options[:attribute] || @name
29
+ @events = EventCollection.new(self)
30
+ @states = StateCollection.new(self)
31
+ @callbacks = { before: [], after: [], failure: [] }
32
+ @namespace = options[:namespace]
33
+ @messages = options[:messages] || {}
34
+ @action = options[:action]
35
+ @use_transactions = options[:use_transactions]
36
+ @initialize_state = options[:initialize]
37
+ @action_hook_defined = false
38
+ self.owner_class = owner_class
39
+
40
+ # Merge with sibling machine configurations
41
+ add_sibling_machine_configs
42
+
43
+ # Define class integration
44
+ define_helpers
45
+ define_scopes(options[:plural])
46
+ after_initialize
47
+
48
+ # Evaluate DSL
49
+ instance_eval(&) if block_given?
50
+ self.initial_state = options[:initial] unless sibling_machines.any?
51
+ end
52
+
53
+ # Creates a copy of this machine in addition to copies of each associated
54
+ # event/states/callback, so that the modifications to those collections do
55
+ # not affect the original machine.
56
+ def initialize_copy(orig) # :nodoc:
57
+ super
58
+
59
+ @events = @events.dup
60
+ @events.machine = self
61
+ @states = @states.dup
62
+ @states.machine = self
63
+ @callbacks = { before: @callbacks[:before].dup, after: @callbacks[:after].dup, failure: @callbacks[:failure].dup }
64
+ end
65
+
66
+ # Sets the class which is the owner of this state machine. Any methods
67
+ # generated by states, events, or other parts of the machine will be defined
68
+ # on the given owner class.
69
+ def owner_class=(klass)
70
+ @owner_class = klass
71
+
72
+ # Create modules for extending the class with state/event-specific methods
73
+ @helper_modules = helper_modules = { instance: HelperModule.new(self, :instance), class: HelperModule.new(self, :class) }
74
+ owner_class.class_eval do
75
+ extend helper_modules[:class]
76
+ include helper_modules[:instance]
77
+ end
78
+
79
+ # Add class-/instance-level methods to the owner class for state initialization
80
+ unless owner_class < StateMachines::InstanceMethods
81
+ owner_class.class_eval do
82
+ extend StateMachines::ClassMethods
83
+ include StateMachines::InstanceMethods
84
+ end
85
+
86
+ define_state_initializer if @initialize_state
87
+ end
88
+
89
+ # Record this machine as matched to the name in the current owner class.
90
+ # This will override any machines mapped to the same name in any superclasses.
91
+ owner_class.state_machines[name] = self
92
+ end
93
+
94
+ # Sets the initial state of the machine. This can be either the static name
95
+ # of a state or a lambda block which determines the initial state at
96
+ # creation time.
97
+ def initial_state=(new_initial_state)
98
+ @initial_state = new_initial_state
99
+ add_states([@initial_state]) unless dynamic_initial_state?
100
+
101
+ # Update all states to reflect the new initial state
102
+ states.each { |state| state.initial = (state.name == @initial_state) }
103
+
104
+ # Output a warning if there are conflicting initial states for the machine's
105
+ # attribute
106
+ initial_state = states.detect(&:initial)
107
+ has_owner_default = !owner_class_attribute_default.nil?
108
+ has_conflicting_default = dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state)
109
+ return unless has_owner_default && has_conflicting_default
110
+
111
+ warn(
112
+ "Both #{owner_class.name} and its #{name.inspect} machine have defined " \
113
+ "a different default for \"#{attribute}\". Use only one or the other for " \
114
+ 'defining defaults to avoid unexpected behaviors.'
115
+ )
116
+ end
117
+
118
+ # Gets the attribute name for the given machine scope.
119
+ def attribute(name = :state)
120
+ name == :state ? @attribute : :"#{self.name}_#{name}"
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module EventMethods
6
+ # Defines one or more events for the machine and the transitions that can
7
+ # be performed when those events are run.
8
+ def event(*names, &)
9
+ options = names.last.is_a?(Hash) ? names.pop : {}
10
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :human_name)
11
+
12
+ # Store the context so that it can be used for / matched against any event
13
+ # that gets added
14
+ @events.context(names, &) if block_given?
15
+
16
+ if names.first.is_a?(Matcher)
17
+ # Add any events referenced in the matcher. When matchers are used,
18
+ # events are not allowed to be configured.
19
+ raise ArgumentError, "Cannot configure events when using matchers (using #{options.inspect})" if options.any?
20
+
21
+ events = add_events(names.first.values)
22
+ else
23
+ events = add_events(names)
24
+
25
+ # Update the configuration for the event(s)
26
+ events.each do |event|
27
+ event.human_name = options[:human_name] if options.include?(:human_name)
28
+
29
+ # Add any states that may have been referenced within the event
30
+ add_states(event.known_states)
31
+ end
32
+ end
33
+
34
+ events.length == 1 ? events.first : events
35
+ end
36
+
37
+ alias on event
38
+
39
+ # Creates a new transition that determines what to change the current state
40
+ # to when an event fires.
41
+ def transition(options)
42
+ raise ArgumentError, 'Must specify :on event' unless options[:on]
43
+
44
+ branches = []
45
+ options = options.dup
46
+ event(*Array(options.delete(:on))) { branches << transition(options) }
47
+
48
+ branches.length == 1 ? branches.first : branches
49
+ end
50
+
51
+ # Gets the list of all possible transition paths from the current state to
52
+ # the given target state. If multiple target states are provided, then
53
+ # this will return all possible paths to those states.
54
+ def paths_for(object, requirements = {})
55
+ PathCollection.new(object, self, requirements)
56
+ end
57
+ end
58
+ end
59
+ end