state_machines 0.6.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +205 -14
  3. data/lib/state_machines/branch.rb +20 -17
  4. data/lib/state_machines/callback.rb +13 -12
  5. data/lib/state_machines/core.rb +3 -3
  6. data/lib/state_machines/core_ext/class/state_machine.rb +2 -0
  7. data/lib/state_machines/core_ext.rb +2 -0
  8. data/lib/state_machines/error.rb +7 -4
  9. data/lib/state_machines/eval_helpers.rb +93 -26
  10. data/lib/state_machines/event.rb +41 -29
  11. data/lib/state_machines/event_collection.rb +6 -5
  12. data/lib/state_machines/extensions.rb +7 -5
  13. data/lib/state_machines/helper_module.rb +3 -1
  14. data/lib/state_machines/integrations/base.rb +3 -1
  15. data/lib/state_machines/integrations.rb +13 -14
  16. data/lib/state_machines/machine/action_hooks.rb +53 -0
  17. data/lib/state_machines/machine/callbacks.rb +59 -0
  18. data/lib/state_machines/machine/class_methods.rb +93 -0
  19. data/lib/state_machines/machine/configuration.rb +124 -0
  20. data/lib/state_machines/machine/event_methods.rb +59 -0
  21. data/lib/state_machines/machine/helper_generators.rb +125 -0
  22. data/lib/state_machines/machine/integration.rb +70 -0
  23. data/lib/state_machines/machine/parsing.rb +77 -0
  24. data/lib/state_machines/machine/rendering.rb +17 -0
  25. data/lib/state_machines/machine/scoping.rb +44 -0
  26. data/lib/state_machines/machine/state_methods.rb +101 -0
  27. data/lib/state_machines/machine/utilities.rb +85 -0
  28. data/lib/state_machines/machine/validation.rb +39 -0
  29. data/lib/state_machines/machine.rb +83 -673
  30. data/lib/state_machines/machine_collection.rb +23 -15
  31. data/lib/state_machines/macro_methods.rb +4 -2
  32. data/lib/state_machines/matcher.rb +8 -5
  33. data/lib/state_machines/matcher_helpers.rb +3 -1
  34. data/lib/state_machines/node_collection.rb +23 -18
  35. data/lib/state_machines/options_validator.rb +72 -0
  36. data/lib/state_machines/path.rb +7 -5
  37. data/lib/state_machines/path_collection.rb +7 -4
  38. data/lib/state_machines/state.rb +76 -47
  39. data/lib/state_machines/state_collection.rb +5 -3
  40. data/lib/state_machines/state_context.rb +11 -8
  41. data/lib/state_machines/stdio_renderer.rb +74 -0
  42. data/lib/state_machines/syntax_validator.rb +57 -0
  43. data/lib/state_machines/test_helper.rb +568 -0
  44. data/lib/state_machines/transition.rb +45 -41
  45. data/lib/state_machines/transition_collection.rb +27 -26
  46. data/lib/state_machines/version.rb +3 -1
  47. data/lib/state_machines.rb +4 -1
  48. metadata +32 -16
  49. data/lib/state_machines/assertions.rb +0 -40
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options_validator'
4
+
1
5
  module StateMachines
2
6
  # Represents a collection of state machines for a class
3
7
  class MachineCollection < Hash
@@ -20,21 +24,25 @@ module StateMachines
20
24
  # * <tt>:to</tt> - A hash to write the initialized state to instead of
21
25
  # writing to the object. Default is to write directly to the object.
22
26
  def initialize_states(object, options = {}, attributes = {})
23
- options.assert_valid_keys( :static, :dynamic, :to)
24
- options = {static: true, dynamic: true}.merge(options)
27
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :static, :dynamic, :to)
28
+ options = { static: true, dynamic: true }.merge(options)
25
29
 
26
30
  result = yield if block_given?
27
31
 
28
- each_value do |machine|
29
- unless machine.dynamic_initial_state?
30
- force = options[:static] == :force || !attributes.keys.map(&:to_sym).include?(machine.attribute)
31
- machine.initialize_state(object, force: force, to: options[:to])
32
+ if options[:static]
33
+ each_value do |machine|
34
+ unless machine.dynamic_initial_state?
35
+ force = options[:static] == :force || !attributes.keys.map(&:to_sym).include?(machine.attribute)
36
+ machine.initialize_state(object, force: force, to: options[:to])
37
+ end
32
38
  end
33
- end if options[:static]
39
+ end
34
40
 
35
- each_value do |machine|
36
- machine.initialize_state(object, force: options[:dynamic] == :force, to: options[:to]) if machine.dynamic_initial_state?
37
- end if options[:dynamic]
41
+ if options[:dynamic]
42
+ each_value do |machine|
43
+ machine.initialize_state(object, force: options[:dynamic] == :force, to: options[:to]) if machine.dynamic_initial_state?
44
+ end
45
+ end
38
46
 
39
47
  result
40
48
  end
@@ -48,7 +56,7 @@ module StateMachines
48
56
  transitions = events.collect do |event_name|
49
57
  # Find the actual event being run
50
58
  event = nil
51
- detect { |name, machine| event = machine.events[event_name, :qualified_name] }
59
+ detect { |_name, machine| event = machine.events[event_name, :qualified_name] }
52
60
 
53
61
  raise(InvalidEvent.new(object, event_name)) unless event
54
62
 
@@ -62,7 +70,7 @@ module StateMachines
62
70
  # Run the events in parallel only if valid transitions were found for
63
71
  # all of them
64
72
  if events.length == transitions.length
65
- TransitionCollection.new(transitions, {use_transactions: resolve_use_transactions, actions: run_action}).perform
73
+ TransitionCollection.new(transitions, { use_transactions: resolve_use_transactions, actions: run_action }).perform
66
74
  else
67
75
  false
68
76
  end
@@ -74,14 +82,14 @@ module StateMachines
74
82
  #
75
83
  # These should only be fired as a result of the action being run.
76
84
  def transitions(object, action, options = {})
77
- transitions = map do |name, machine|
85
+ transitions = map do |_name, machine|
78
86
  machine.events.attribute_transition_for(object, true) if machine.action == action
79
87
  end
80
88
 
81
- AttributeTransitionCollection.new(transitions.compact, {use_transactions: resolve_use_transactions}.merge(options))
89
+ AttributeTransitionCollection.new(transitions.compact, { use_transactions: resolve_use_transactions }.merge(options))
82
90
  end
83
91
 
84
- protected
92
+ protected
85
93
 
86
94
  def resolve_use_transactions
87
95
  use_transactions = nil
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # A state machine is a model of behavior composed of states, events, and
2
4
  # transitions. This helper adds support for defining this type of
3
5
  # functionality on any Ruby class.
@@ -513,8 +515,8 @@ module StateMachines
513
515
  # See StateMachines::Machine for more information about using integrations
514
516
  # and the individual integration docs for information about the actual
515
517
  # scopes that are generated.
516
- def state_machine(*args, &block)
517
- StateMachines::Machine.find_or_create(self, *args, &block)
518
+ def state_machine(*, &)
519
+ StateMachines::Machine.find_or_create(self, *, &)
518
520
  end
519
521
  end
520
522
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # Provides a general strategy pattern for determining whether a match is found
3
5
  # for a value. The algorithm that actually determines the match depends on
@@ -30,12 +32,13 @@ module StateMachines
30
32
  # matcher = StateMachines::AllMatcher.instance - [:parked, :idling]
31
33
  # matcher.matches?(:parked) # => false
32
34
  # matcher.matches?(:first_gear) # => true
33
- def -(blacklist)
34
- BlacklistMatcher.new(blacklist)
35
+ def -(other)
36
+ BlacklistMatcher.new(other)
35
37
  end
38
+ alias except -
36
39
 
37
40
  # Always returns true
38
- def matches?(value, context = {})
41
+ def matches?(_value, _context = {})
39
42
  true
40
43
  end
41
44
 
@@ -60,7 +63,7 @@ module StateMachines
60
63
  # matcher = StateMachines::WhitelistMatcher.new([:parked, :idling])
61
64
  # matcher.matches?(:parked) # => true
62
65
  # matcher.matches?(:first_gear) # => false
63
- def matches?(value, context = {})
66
+ def matches?(value, _context = {})
64
67
  values.include?(value)
65
68
  end
66
69
 
@@ -80,7 +83,7 @@ module StateMachines
80
83
  # matcher = StateMachines::BlacklistMatcher.new([:parked, :idling])
81
84
  # matcher.matches?(:parked) # => false
82
85
  # matcher.matches?(:first_gear) # => true
83
- def matches?(value, context = {})
86
+ def matches?(value, _context = {})
84
87
  !values.include?(value)
85
88
  end
86
89
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # Provides a set of helper methods for generating matchers
3
5
  module MatcherHelpers
@@ -28,7 +30,7 @@ module StateMachines
28
30
  def all
29
31
  AllMatcher.instance
30
32
  end
31
- alias_method :any, :all
33
+ alias any all
32
34
 
33
35
  # Represents a state that matches the original +from+ state. This is useful
34
36
  # for defining transitions which are loopbacks.
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options_validator'
4
+
1
5
  module StateMachines
2
6
  # Represents a collection of nodes in a state machine, be it events or states.
3
7
  # Nodes will not differentiate between the String and Symbol versions of the
@@ -16,17 +20,16 @@ module StateMachines
16
20
  # hashed indices for in order to perform quick lookups. Default is to
17
21
  # index by the :name attribute
18
22
  def initialize(machine, options = {})
19
- options.assert_valid_keys(:index)
23
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :index)
20
24
  options = { index: :name }.merge(options)
21
25
 
22
26
  @machine = machine
23
27
  @nodes = []
24
28
  @index_names = Array(options[:index])
25
- @indices = @index_names.reduce({}) do |indices, name|
29
+ @indices = @index_names.each_with_object({}) do |name, indices|
26
30
  indices[name] = {}
27
31
  indices[:"#{name}_to_s"] = {}
28
32
  indices[:"#{name}_to_sym"] = {}
29
- indices
30
33
  end
31
34
  @default_index = Array(options[:index]).first
32
35
  @contexts = []
@@ -34,14 +37,16 @@ module StateMachines
34
37
 
35
38
  # Creates a copy of this collection such that modifications don't affect
36
39
  # the original collection
37
- def initialize_copy(orig) #:nodoc:
40
+ def initialize_copy(orig) # :nodoc:
38
41
  super
39
42
 
40
43
  nodes = @nodes
41
44
  contexts = @contexts
42
45
  @nodes = []
43
46
  @contexts = []
44
- @indices = @indices.reduce({}) { |indices, (name, *)| indices[name] = {}; indices }
47
+ @indices = @indices.each_with_object({}) do |(name, *), indices|
48
+ indices[name] = {}
49
+ end
45
50
 
46
51
  # Add nodes *prior* to copying over the contexts so that they don't get
47
52
  # evaluated multiple times
@@ -112,8 +117,8 @@ module StateMachines
112
117
  # ...produces:
113
118
  #
114
119
  # parked -- idling --
115
- def each
116
- @nodes.each { |node| yield node }
120
+ def each(&)
121
+ @nodes.each(&)
117
122
  self
118
123
  end
119
124
 
@@ -141,9 +146,9 @@ module StateMachines
141
146
  # If the key cannot be found, then nil will be returned.
142
147
  def [](key, index_name = @default_index)
143
148
  index(index_name)[key] ||
144
- index(:"#{index_name}_to_s")[key.to_s] ||
145
- to_sym?(key) && index(:"#{index_name}_to_sym")[:"#{key}"] ||
146
- nil
149
+ index(:"#{index_name}_to_s")[key.to_s] ||
150
+ (to_sym?(key) && index(:"#{index_name}_to_sym")[:"#{key}"]) ||
151
+ nil
147
152
  end
148
153
 
149
154
  # Gets the node indexed by the given key. By default, this will look up the
@@ -159,17 +164,17 @@ module StateMachines
159
164
  #
160
165
  # collection['invalid', :value] # => IndexError: "invalid" is an invalid value
161
166
  def fetch(key, index_name = @default_index)
162
- self[key, index_name] || fail(IndexError, "#{key.inspect} is an invalid #{index_name}")
167
+ self[key, index_name] || raise(IndexError, "#{key.inspect} is an invalid #{index_name}")
163
168
  end
164
169
 
165
- protected
170
+ protected
166
171
 
167
172
  # Gets the given index. If the index does not exist, then an ArgumentError
168
173
  # is raised.
169
174
  def index(name)
170
- fail ArgumentError, 'No indices configured' unless @indices.any?
175
+ raise ArgumentError, 'No indices configured' unless @indices.any?
171
176
 
172
- @indices[name] || fail(ArgumentError, "Invalid index: #{name.inspect}")
177
+ @indices[name] || raise(ArgumentError, "Invalid index: #{name.inspect}")
173
178
  end
174
179
 
175
180
  # Gets the value for the given attribute on the node
@@ -201,10 +206,10 @@ module StateMachines
201
206
  new_key = value(node, name)
202
207
 
203
208
  # Only replace the key if it's changed
204
- if old_key != new_key
205
- remove_from_index(name, old_key)
206
- add_to_index(name, new_key, node)
207
- end
209
+ return unless old_key != new_key
210
+
211
+ remove_from_index(name, old_key)
212
+ add_to_index(name, new_key, node)
208
213
  end
209
214
 
210
215
  # Determines whether the given value can be converted to a symbol
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ # Define the module if it doesn't exist yet
5
+ # Module for validating options without monkey-patching Hash
6
+ # Provides the same functionality as the Hash monkey patch but in a cleaner way
7
+ module OptionsValidator
8
+ class << self
9
+ # Validates that all keys in the options hash are in the list of valid keys
10
+ #
11
+ # @param options [Hash] The options hash to validate
12
+ # @param valid_keys [Array<Symbol>] List of valid key names
13
+ # @param caller_info [String] Information about the calling method for better error messages
14
+ # @raise [ArgumentError] If any invalid keys are found
15
+ def assert_valid_keys!(options, *valid_keys, caller_info: nil)
16
+ return if options.empty?
17
+
18
+ valid_keys.flatten!
19
+ invalid_keys = options.keys - valid_keys
20
+
21
+ return if invalid_keys.empty?
22
+
23
+ caller_context = caller_info ? " in #{caller_info}" : ''
24
+ raise ArgumentError, "Unknown key#{'s' if invalid_keys.length > 1}: #{invalid_keys.map(&:inspect).join(', ')}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}#{caller_context}"
25
+ end
26
+
27
+ # Validates that at most one of the exclusive keys is present in the options hash
28
+ #
29
+ # @param options [Hash] The options hash to validate
30
+ # @param exclusive_keys [Array<Symbol>] List of mutually exclusive keys
31
+ # @param caller_info [String] Information about the calling method for better error messages
32
+ # @raise [ArgumentError] If more than one exclusive key is found
33
+ def assert_exclusive_keys!(options, *exclusive_keys, caller_info: nil)
34
+ return if options.empty?
35
+
36
+ conflicting_keys = exclusive_keys & options.keys
37
+ return if conflicting_keys.length <= 1
38
+
39
+ caller_context = caller_info ? " in #{caller_info}" : ''
40
+ raise ArgumentError, "Conflicting keys: #{conflicting_keys.join(', ')}#{caller_context}"
41
+ end
42
+
43
+ # Validates options using a more convenient interface that works with both
44
+ # hash-style and kwargs-style method definitions
45
+ #
46
+ # @param valid_keys [Array<Symbol>] List of valid key names
47
+ # @param exclusive_key_groups [Array<Array<Symbol>>] Groups of mutually exclusive keys
48
+ # @param caller_info [String] Information about the calling method
49
+ # @return [Proc] A validation proc that can be called with options
50
+ def validator(valid_keys: [], exclusive_key_groups: [], caller_info: nil)
51
+ proc do |options|
52
+ assert_valid_keys!(options, *valid_keys, caller_info: caller_info) unless valid_keys.empty?
53
+
54
+ exclusive_key_groups.each do |group|
55
+ assert_exclusive_keys!(options, *group, caller_info: caller_info)
56
+ end
57
+ end
58
+ end
59
+
60
+ # Helper method for backwards compatibility - allows gradual migration
61
+ # from Hash monkey patch to this module
62
+ #
63
+ # @param options [Hash] The options to validate
64
+ # @param valid_keys [Array<Symbol>] Valid keys
65
+ # @return [Hash] The same options hash (for chaining)
66
+ def validate_and_return(options, *valid_keys)
67
+ assert_valid_keys!(options, *valid_keys)
68
+ options
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options_validator'
4
+
1
5
  module StateMachines
2
6
  # A path represents a sequence of transitions that can be run for a particular
3
7
  # object. Paths can walk to new transitions, revealing all of the possible
4
8
  # branches that can be encountered in the object's state machine.
5
9
  class Path < Array
6
-
7
-
8
10
  # The object whose state machine is being walked
9
11
  attr_reader :object
10
12
 
@@ -20,7 +22,7 @@ module StateMachines
20
22
  # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
21
23
  # conditionals defined for each one
22
24
  def initialize(object, machine, options = {})
23
- options.assert_valid_keys(:target, :guard)
25
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :target, :guard)
24
26
 
25
27
  @object = object
26
28
  @machine = machine
@@ -28,7 +30,7 @@ module StateMachines
28
30
  @guard = options[:guard]
29
31
  end
30
32
 
31
- def initialize_copy(orig) #:nodoc:
33
+ def initialize_copy(orig) # :nodoc:
32
34
  super
33
35
  @transitions = nil
34
36
  end
@@ -86,7 +88,7 @@ module StateMachines
86
88
  !empty? && (@target ? to_name == @target : transitions.empty?)
87
89
  end
88
90
 
89
- private
91
+ private
90
92
 
91
93
  # Calculates the number of times the given state has been walked to
92
94
  def times_walked_to(state)
@@ -1,8 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options_validator'
4
+
1
5
  module StateMachines
2
6
  # Represents a collection of paths that are generated based on a set of
3
7
  # requirements regarding what states to start and end on
4
8
  class PathCollection < Array
5
-
6
9
  # The object whose state machine is being walked
7
10
  attr_reader :object
8
11
 
@@ -24,8 +27,8 @@ module StateMachines
24
27
  # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
25
28
  # conditionals defined for each one
26
29
  def initialize(object, machine, options = {})
27
- options = {deep: false, from: machine.states.match!(object).name}.merge(options)
28
- options.assert_valid_keys( :from, :to, :deep, :guard)
30
+ options = { deep: false, from: machine.states.match!(object).name }.merge(options)
31
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :deep, :guard)
29
32
 
30
33
  @object = object
31
34
  @machine = machine
@@ -67,7 +70,7 @@ module StateMachines
67
70
  flat_map(&:events).uniq
68
71
  end
69
72
 
70
- private
73
+ private
71
74
 
72
75
  # Gets the initial set of paths to walk
73
76
  def initial_paths
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options_validator'
4
+
1
5
  module StateMachines
2
6
  # A state defines a value that an attribute can be in after being transitioned
3
7
  # 0 or more times. States can represent a value of any type in Ruby, though
@@ -8,7 +12,6 @@ module StateMachines
8
12
  # StateMachines::Machine#state for more information about how state-driven
9
13
  # behavior can be utilized.
10
14
  class State
11
-
12
15
  # The state machine for which this state is defined
13
16
  attr_reader :machine
14
17
 
@@ -31,7 +34,7 @@ module StateMachines
31
34
 
32
35
  # Whether or not this state is the initial state to use for new objects
33
36
  attr_accessor :initial
34
- alias_method :initial?, :initial
37
+ alias initial? initial
35
38
 
36
39
  # A custom lambda block for determining whether a given value matches this
37
40
  # state
@@ -50,38 +53,58 @@ module StateMachines
50
53
  # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
51
54
  # By default, the configured value is matched.
52
55
  # * <tt>:human_name</tt> - The human-readable version of this state's name
53
- def initialize(machine, name, options = {}) #:nodoc:
54
- options.assert_valid_keys(:initial, :value, :cache, :if, :human_name)
56
+ def initialize(machine, name, options = nil, initial: false, value: :__not_provided__, cache: nil, if: nil, human_name: nil, **extra_options) # :nodoc:
57
+ # Handle both old hash style and new kwargs style for backward compatibility
58
+ if options.is_a?(Hash)
59
+ # Old style: initialize(machine, name, {initial: true, value: 'foo'})
60
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :initial, :value, :cache, :if, :human_name)
61
+ initial = options.fetch(:initial, false)
62
+ value = options.include?(:value) ? options[:value] : :__not_provided__
63
+ cache = options[:cache]
64
+ if_condition = options[:if]
65
+ human_name = options[:human_name]
66
+ else
67
+ # New style: initialize(machine, name, initial: true, value: 'foo')
68
+ # options parameter should be nil in this case
69
+ raise ArgumentError, "Unexpected positional argument: #{options.inspect}" unless options.nil?
70
+
71
+ StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :initial, :value, :cache, :if, :human_name) unless extra_options.empty?
72
+ if_condition = binding.local_variable_get(:if) # 'if' is a keyword, need special handling
73
+ end
55
74
 
56
75
  @machine = machine
57
76
  @name = name
58
77
  @qualified_name = name && machine.namespace ? :"#{machine.namespace}_#{name}" : name
59
- @human_name = options[:human_name] || (@name ? @name.to_s.tr('_', ' ') : 'nil')
60
- @value = options.include?(:value) ? options[:value] : name&.to_s
61
- @cache = options[:cache]
62
- @matcher = options[:if]
63
- @initial = options[:initial] == true
78
+ @human_name = human_name || (@name ? @name.to_s.tr('_', ' ') : 'nil')
79
+ @value = value == :__not_provided__ ? name&.to_s : value
80
+ @cache = cache
81
+ @matcher = if_condition
82
+ @initial = initial == true
64
83
  @context = StateContext.new(self)
65
84
 
66
- if name
67
- conflicting_machines = machine.owner_class.state_machines.select { |other_name, other_machine| other_machine != machine && other_machine.states[qualified_name, :qualified_name] }
68
-
69
- # Output a warning if another machine has a conflicting qualified name
70
- # for a different attribute
71
- if (conflict = conflicting_machines.detect { |_other_name, other_machine| other_machine.attribute != machine.attribute })
72
- _name, other_machine = conflict
73
- warn "State #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
74
- elsif conflicting_machines.empty?
75
- # Only bother adding predicates when another machine for the same
76
- # attribute hasn't already done so
77
- add_predicate
78
- end
85
+ return unless name
86
+
87
+ conflicting_machines = machine.owner_class.state_machines.select do |_other_name, other_machine|
88
+ other_machine != machine && other_machine.states[qualified_name, :qualified_name]
89
+ end
90
+
91
+ # Output a warning if another machine has a conflicting qualified name
92
+ # for a different attribute
93
+ if (conflict = conflicting_machines.detect do |_other_name, other_machine|
94
+ other_machine.attribute != machine.attribute
95
+ end)
96
+ _name, other_machine = conflict
97
+ warn "State #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
98
+ elsif conflicting_machines.empty?
99
+ # Only bother adding predicates when another machine for the same
100
+ # attribute hasn't already done so
101
+ add_predicate
79
102
  end
80
103
  end
81
104
 
82
105
  # Creates a copy of this state, excluding the context to prevent conflicts
83
106
  # across different machines.
84
- def initialize_copy(orig) #:nodoc:
107
+ def initialize_copy(orig) # :nodoc:
85
108
  super
86
109
  @context = StateContext.new(self)
87
110
  end
@@ -96,7 +119,7 @@ module StateMachines
96
119
  # Any objects in a final state will remain so forever given the current
97
120
  # machine's definition.
98
121
  def final?
99
- !machine.events.any? do |event|
122
+ machine.events.none? do |event|
100
123
  event.branches.any? do |branch|
101
124
  branch.state_requirements.any? do |requirement|
102
125
  requirement[:from].matches?(name) && !requirement[:to].matches?(name, from: name)
@@ -126,7 +149,7 @@ module StateMachines
126
149
  # description or just the internal name
127
150
  def description(options = {})
128
151
  label = options[:human_name] ? human_name : name
129
- description = label ? label.to_s : label.inspect
152
+ description = +(label ? label.to_s : label.inspect)
130
153
  description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s
131
154
  description
132
155
  end
@@ -178,27 +201,27 @@ module StateMachines
178
201
  #
179
202
  # This can be called multiple times. Each time a new context is created,
180
203
  # a new module will be included in the owner class.
181
- def context(&block)
204
+ def context(&)
182
205
  # Include the context
183
206
  context = @context
184
207
  machine.owner_class.class_eval { include context }
185
208
 
186
209
  # Evaluate the method definitions and track which ones were added
187
210
  old_methods = context_methods
188
- context.class_eval(&block)
189
- new_methods = context_methods.to_a.select { |(name, method)| old_methods[name] != method }
211
+ context.class_eval(&)
212
+ new_methods = context_methods.to_a.reject { |(name, method)| old_methods[name] == method }
190
213
 
191
214
  # Alias new methods so that the only execute when the object is in this state
192
215
  new_methods.each do |(method_name, _method)|
193
216
  context_name = context_name_for(method_name)
194
- context.class_eval <<-end_eval, __FILE__, __LINE__ + 1
217
+ context.class_eval <<-END_EVAL, __FILE__, __LINE__ + 1
195
218
  alias_method :"#{context_name}", :#{method_name}
196
219
  def #{method_name}(*args, &block)
197
220
  state = self.class.state_machine(#{machine.name.inspect}).states.fetch(#{name.inspect})
198
221
  options = {:method_missing => lambda {super(*args, &block)}, :method_name => #{method_name.inspect}}
199
222
  state.call(self, :"#{context_name}", *(args + [options]), &block)
200
223
  end
201
- end_eval
224
+ END_EVAL
202
225
  end
203
226
 
204
227
  true
@@ -216,31 +239,30 @@ module StateMachines
216
239
  #
217
240
  # If the method has never been defined for this state, then a NoMethodError
218
241
  # will be raised.
219
- def call(object, method, *args, &block)
242
+ def call(object, method, *args, &)
220
243
  options = args.last.is_a?(Hash) ? args.pop : {}
221
- options = {method_name: method}.merge(options)
244
+ options = { method_name: method }.merge(options)
222
245
  state = machine.states.match!(object)
223
246
 
224
247
  if state == self && object.respond_to?(method)
225
- object.send(method, *args, &block)
226
- elsif method_missing = options[:method_missing]
248
+ object.send(method, *args, &)
249
+ elsif (method_missing = options[:method_missing])
227
250
  # Dispatch to the superclass since the object either isn't in this state
228
251
  # or this state doesn't handle the method
229
252
  begin
230
253
  method_missing.call
231
- rescue NoMethodError => ex
232
- if ex.name.to_s == options[:method_name].to_s && ex.args == args
233
- # No valid context for this method
234
- raise InvalidContext.new(object, "State #{state.name.inspect} for #{machine.name.inspect} is not a valid context for calling ##{options[:method_name]}")
235
- else
236
- raise
237
- end
254
+ rescue NoMethodError => e
255
+ raise unless e.name.to_s == options[:method_name].to_s && e.args == args
256
+
257
+ # No valid context for this method
258
+ raise InvalidContext.new(object,
259
+ "State #{state.name.inspect} for #{machine.name.inspect} is not a valid context for calling ##{options[:method_name]}")
238
260
  end
239
261
  end
240
262
  end
241
263
 
242
- def draw(graph, options = {})
243
- fail NotImplementedError
264
+ def draw(graph, options = {}, io = $stdout)
265
+ machine.renderer.draw_state(self, graph, options, io)
244
266
  end
245
267
 
246
268
  # Generates a nicely formatted description of this state's contents.
@@ -254,7 +276,7 @@ module StateMachines
254
276
  "#<#{self.class} #{attributes.map { |attr, value| "#{attr}=#{value.inspect}" } * ' '}>"
255
277
  end
256
278
 
257
- private
279
+ private
258
280
 
259
281
  # Should the value be cached after it's evaluated for the first time?
260
282
  def cache_value?
@@ -264,9 +286,16 @@ module StateMachines
264
286
  # Adds a predicate method to the owner class so long as a name has
265
287
  # actually been configured for the state
266
288
  def add_predicate
267
- # Checks whether the current value matches this state
268
- machine.define_helper(:instance, "#{qualified_name}?") do |machine, object|
269
- machine.states.matches?(object, name)
289
+ predicate_method = "#{qualified_name}?"
290
+
291
+ if machine.send(:owner_class_ancestor_has_method?, :instance, predicate_method)
292
+ warn "Instance method #{predicate_method.inspect} is already defined in #{machine.owner_class.ancestors.first.inspect}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true."
293
+ elsif machine.send(:owner_class_has_method?, :instance, predicate_method)
294
+ warn "Instance method #{predicate_method.inspect} is already defined in #{machine.owner_class.inspect}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true."
295
+ else
296
+ machine.define_helper(:instance, predicate_method) do |machine, object|
297
+ machine.states.matches?(object, name)
298
+ end
270
299
  end
271
300
  end
272
301
 
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # Represents a collection of states in a state machine
3
5
  class StateCollection < NodeCollection
4
- def initialize(machine) #:nodoc:
5
- super(machine, index: [:name, :qualified_name, :value])
6
+ def initialize(machine) # :nodoc:
7
+ super(machine, index: %i[name qualified_name value])
6
8
  end
7
9
 
8
10
  # Determines whether the given object is in a specific state. If the
@@ -101,7 +103,7 @@ module StateMachines
101
103
  order
102
104
  end
103
105
 
104
- private
106
+ private
105
107
 
106
108
  # Gets the value for the given attribute on the node
107
109
  def value(node, attribute)