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
@@ -25,20 +25,24 @@ module StateMachines
25
25
  # writing to the object. Default is to write directly to the object.
26
26
  def initialize_states(object, options = {}, attributes = {})
27
27
  StateMachines::OptionsValidator.assert_valid_keys!(options, :static, :dynamic, :to)
28
- options = {static: true, dynamic: true}.merge(options)
28
+ options = { static: true, dynamic: true }.merge(options)
29
29
 
30
30
  result = yield if block_given?
31
31
 
32
- each_value do |machine|
33
- unless machine.dynamic_initial_state?
34
- force = options[:static] == :force || !attributes.keys.map(&:to_sym).include?(machine.attribute)
35
- 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
36
38
  end
37
- end if options[:static]
39
+ end
38
40
 
39
- each_value do |machine|
40
- machine.initialize_state(object, force: options[:dynamic] == :force, to: options[:to]) if machine.dynamic_initial_state?
41
- 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
42
46
 
43
47
  result
44
48
  end
@@ -52,7 +56,7 @@ module StateMachines
52
56
  transitions = events.collect do |event_name|
53
57
  # Find the actual event being run
54
58
  event = nil
55
- detect { |name, machine| event = machine.events[event_name, :qualified_name] }
59
+ detect { |_name, machine| event = machine.events[event_name, :qualified_name] }
56
60
 
57
61
  raise(InvalidEvent.new(object, event_name)) unless event
58
62
 
@@ -66,7 +70,7 @@ module StateMachines
66
70
  # Run the events in parallel only if valid transitions were found for
67
71
  # all of them
68
72
  if events.length == transitions.length
69
- 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
70
74
  else
71
75
  false
72
76
  end
@@ -78,14 +82,14 @@ module StateMachines
78
82
  #
79
83
  # These should only be fired as a result of the action being run.
80
84
  def transitions(object, action, options = {})
81
- transitions = map do |name, machine|
85
+ transitions = map do |_name, machine|
82
86
  machine.events.attribute_transition_for(object, true) if machine.action == action
83
87
  end
84
88
 
85
- AttributeTransitionCollection.new(transitions.compact, {use_transactions: resolve_use_transactions}.merge(options))
89
+ AttributeTransitionCollection.new(transitions.compact, { use_transactions: resolve_use_transactions }.merge(options))
86
90
  end
87
91
 
88
- protected
92
+ protected
89
93
 
90
94
  def resolve_use_transactions
91
95
  use_transactions = nil
@@ -515,8 +515,8 @@ module StateMachines
515
515
  # See StateMachines::Machine for more information about using integrations
516
516
  # and the individual integration docs for information about the actual
517
517
  # scopes that are generated.
518
- def state_machine(*args, &block)
519
- StateMachines::Machine.find_or_create(self, *args, &block)
518
+ def state_machine(*, &)
519
+ StateMachines::Machine.find_or_create(self, *, &)
520
520
  end
521
521
  end
522
522
  end
@@ -32,13 +32,13 @@ module StateMachines
32
32
  # matcher = StateMachines::AllMatcher.instance - [:parked, :idling]
33
33
  # matcher.matches?(:parked) # => false
34
34
  # matcher.matches?(:first_gear) # => true
35
- def -(blacklist)
36
- BlacklistMatcher.new(blacklist)
35
+ def -(other)
36
+ BlacklistMatcher.new(other)
37
37
  end
38
- alias_method :except, :-
38
+ alias except -
39
39
 
40
40
  # Always returns true
41
- def matches?(value, context = {})
41
+ def matches?(_value, _context = {})
42
42
  true
43
43
  end
44
44
 
@@ -63,7 +63,7 @@ module StateMachines
63
63
  # matcher = StateMachines::WhitelistMatcher.new([:parked, :idling])
64
64
  # matcher.matches?(:parked) # => true
65
65
  # matcher.matches?(:first_gear) # => false
66
- def matches?(value, context = {})
66
+ def matches?(value, _context = {})
67
67
  values.include?(value)
68
68
  end
69
69
 
@@ -83,7 +83,7 @@ module StateMachines
83
83
  # matcher = StateMachines::BlacklistMatcher.new([:parked, :idling])
84
84
  # matcher.matches?(:parked) # => false
85
85
  # matcher.matches?(:first_gear) # => true
86
- def matches?(value, context = {})
86
+ def matches?(value, _context = {})
87
87
  !values.include?(value)
88
88
  end
89
89
 
@@ -30,7 +30,7 @@ module StateMachines
30
30
  def all
31
31
  AllMatcher.instance
32
32
  end
33
- alias_method :any, :all
33
+ alias any all
34
34
 
35
35
  # Represents a state that matches the original +from+ state. This is useful
36
36
  # for defining transitions which are loopbacks.
@@ -26,11 +26,10 @@ module StateMachines
26
26
  @machine = machine
27
27
  @nodes = []
28
28
  @index_names = Array(options[:index])
29
- @indices = @index_names.reduce({}) do |indices, name|
29
+ @indices = @index_names.each_with_object({}) do |name, indices|
30
30
  indices[name] = {}
31
31
  indices[:"#{name}_to_s"] = {}
32
32
  indices[:"#{name}_to_sym"] = {}
33
- indices
34
33
  end
35
34
  @default_index = Array(options[:index]).first
36
35
  @contexts = []
@@ -38,14 +37,16 @@ module StateMachines
38
37
 
39
38
  # Creates a copy of this collection such that modifications don't affect
40
39
  # the original collection
41
- def initialize_copy(orig) #:nodoc:
40
+ def initialize_copy(orig) # :nodoc:
42
41
  super
43
42
 
44
43
  nodes = @nodes
45
44
  contexts = @contexts
46
45
  @nodes = []
47
46
  @contexts = []
48
- @indices = @indices.reduce({}) { |indices, (name, *)| indices[name] = {}; indices }
47
+ @indices = @indices.each_with_object({}) do |(name, *), indices|
48
+ indices[name] = {}
49
+ end
49
50
 
50
51
  # Add nodes *prior* to copying over the contexts so that they don't get
51
52
  # evaluated multiple times
@@ -116,8 +117,8 @@ module StateMachines
116
117
  # ...produces:
117
118
  #
118
119
  # parked -- idling --
119
- def each
120
- @nodes.each { |node| yield node }
120
+ def each(&)
121
+ @nodes.each(&)
121
122
  self
122
123
  end
123
124
 
@@ -145,9 +146,9 @@ module StateMachines
145
146
  # If the key cannot be found, then nil will be returned.
146
147
  def [](key, index_name = @default_index)
147
148
  index(index_name)[key] ||
148
- index(:"#{index_name}_to_s")[key.to_s] ||
149
- to_sym?(key) && index(:"#{index_name}_to_sym")[:"#{key}"] ||
150
- nil
149
+ index(:"#{index_name}_to_s")[key.to_s] ||
150
+ (to_sym?(key) && index(:"#{index_name}_to_sym")[:"#{key}"]) ||
151
+ nil
151
152
  end
152
153
 
153
154
  # Gets the node indexed by the given key. By default, this will look up the
@@ -163,17 +164,17 @@ module StateMachines
163
164
  #
164
165
  # collection['invalid', :value] # => IndexError: "invalid" is an invalid value
165
166
  def fetch(key, index_name = @default_index)
166
- 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}")
167
168
  end
168
169
 
169
- protected
170
+ protected
170
171
 
171
172
  # Gets the given index. If the index does not exist, then an ArgumentError
172
173
  # is raised.
173
174
  def index(name)
174
- fail ArgumentError, 'No indices configured' unless @indices.any?
175
+ raise ArgumentError, 'No indices configured' unless @indices.any?
175
176
 
176
- @indices[name] || fail(ArgumentError, "Invalid index: #{name.inspect}")
177
+ @indices[name] || raise(ArgumentError, "Invalid index: #{name.inspect}")
177
178
  end
178
179
 
179
180
  # Gets the value for the given attribute on the node
@@ -205,10 +206,10 @@ module StateMachines
205
206
  new_key = value(node, name)
206
207
 
207
208
  # Only replace the key if it's changed
208
- if old_key != new_key
209
- remove_from_index(name, old_key)
210
- add_to_index(name, new_key, node)
211
- end
209
+ return unless old_key != new_key
210
+
211
+ remove_from_index(name, old_key)
212
+ add_to_index(name, new_key, node)
212
213
  end
213
214
 
214
215
  # Determines whether the given value can be converted to a symbol
@@ -7,8 +7,6 @@ module StateMachines
7
7
  # object. Paths can walk to new transitions, revealing all of the possible
8
8
  # branches that can be encountered in the object's state machine.
9
9
  class Path < Array
10
-
11
-
12
10
  # The object whose state machine is being walked
13
11
  attr_reader :object
14
12
 
@@ -32,7 +30,7 @@ module StateMachines
32
30
  @guard = options[:guard]
33
31
  end
34
32
 
35
- def initialize_copy(orig) #:nodoc:
33
+ def initialize_copy(orig) # :nodoc:
36
34
  super
37
35
  @transitions = nil
38
36
  end
@@ -90,7 +88,7 @@ module StateMachines
90
88
  !empty? && (@target ? to_name == @target : transitions.empty?)
91
89
  end
92
90
 
93
- private
91
+ private
94
92
 
95
93
  # Calculates the number of times the given state has been walked to
96
94
  def times_walked_to(state)
@@ -6,7 +6,6 @@ module StateMachines
6
6
  # Represents a collection of paths that are generated based on a set of
7
7
  # requirements regarding what states to start and end on
8
8
  class PathCollection < Array
9
-
10
9
  # The object whose state machine is being walked
11
10
  attr_reader :object
12
11
 
@@ -28,7 +27,7 @@ module StateMachines
28
27
  # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
29
28
  # conditionals defined for each one
30
29
  def initialize(object, machine, options = {})
31
- options = {deep: false, from: machine.states.match!(object).name}.merge(options)
30
+ options = { deep: false, from: machine.states.match!(object).name }.merge(options)
32
31
  StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :deep, :guard)
33
32
 
34
33
  @object = object
@@ -71,7 +70,7 @@ module StateMachines
71
70
  flat_map(&:events).uniq
72
71
  end
73
72
 
74
- private
73
+ private
75
74
 
76
75
  # Gets the initial set of paths to walk
77
76
  def initial_paths
@@ -67,8 +67,9 @@ module StateMachines
67
67
  # New style: initialize(machine, name, initial: true, value: 'foo')
68
68
  # options parameter should be nil in this case
69
69
  raise ArgumentError, "Unexpected positional argument: #{options.inspect}" unless options.nil?
70
+
70
71
  StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :initial, :value, :cache, :if, :human_name) unless extra_options.empty?
71
- if_condition = binding.local_variable_get(:if) # 'if' is a keyword, need special handling
72
+ if_condition = binding.local_variable_get(:if) # 'if' is a keyword, need special handling
72
73
  end
73
74
 
74
75
  @machine = machine
@@ -200,14 +201,14 @@ module StateMachines
200
201
  #
201
202
  # This can be called multiple times. Each time a new context is created,
202
203
  # a new module will be included in the owner class.
203
- def context(&block)
204
+ def context(&)
204
205
  # Include the context
205
206
  context = @context
206
207
  machine.owner_class.class_eval { include context }
207
208
 
208
209
  # Evaluate the method definitions and track which ones were added
209
210
  old_methods = context_methods
210
- context.class_eval(&block)
211
+ context.class_eval(&)
211
212
  new_methods = context_methods.to_a.reject { |(name, method)| old_methods[name] == method }
212
213
 
213
214
  # Alias new methods so that the only execute when the object is in this state
@@ -238,13 +239,13 @@ module StateMachines
238
239
  #
239
240
  # If the method has never been defined for this state, then a NoMethodError
240
241
  # will be raised.
241
- def call(object, method, *args, &block)
242
+ def call(object, method, *args, &)
242
243
  options = args.last.is_a?(Hash) ? args.pop : {}
243
244
  options = { method_name: method }.merge(options)
244
245
  state = machine.states.match!(object)
245
246
 
246
247
  if state == self && object.respond_to?(method)
247
- object.send(method, *args, &block)
248
+ object.send(method, *args, &)
248
249
  elsif (method_missing = options[:method_missing])
249
250
  # Dispatch to the superclass since the object either isn't in this state
250
251
  # or this state doesn't handle the method
@@ -3,8 +3,8 @@
3
3
  module StateMachines
4
4
  # Represents a collection of states in a state machine
5
5
  class StateCollection < NodeCollection
6
- def initialize(machine) #:nodoc:
7
- super(machine, index: [:name, :qualified_name, :value])
6
+ def initialize(machine) # :nodoc:
7
+ super(machine, index: %i[name qualified_name value])
8
8
  end
9
9
 
10
10
  # Determines whether the given object is in a specific state. If the
@@ -103,7 +103,7 @@ module StateMachines
103
103
  order
104
104
  end
105
105
 
106
- private
106
+ private
107
107
 
108
108
  # Gets the value for the given attribute on the node
109
109
  def value(node, attribute)
@@ -54,7 +54,6 @@ module StateMachines
54
54
  # vehicle.simulate = true
55
55
  # vehicle.moving? # => false
56
56
  class StateContext < Module
57
-
58
57
  include EvalHelpers
59
58
 
60
59
  # The state machine for which this context's state is defined
@@ -70,7 +69,7 @@ module StateMachines
70
69
 
71
70
  state_name = state.name
72
71
  machine_name = machine.name
73
- @condition = lambda { |object| object.class.state_machine(machine_name).states.matches?(object, state_name) }
72
+ @condition = ->(object) { object.class.state_machine(machine_name).states.matches?(object, state_name) }
74
73
  end
75
74
 
76
75
  # Creates a new transition that determines what to change the current state
@@ -94,11 +93,11 @@ module StateMachines
94
93
  raise ArgumentError, 'Must specify :on event' unless options[:on]
95
94
  raise ArgumentError, 'Must specify either :to or :from state' unless !options[:to] ^ !options[:from]
96
95
 
97
- machine.transition(options.merge(options[:to] ? {from: state.name} : {to: state.name}))
96
+ machine.transition(options.merge(options[:to] ? { from: state.name } : { to: state.name }))
98
97
  end
99
98
 
100
99
  # Hooks in condition-merging to methods that don't exist in this module
101
- def method_missing(*args, &block)
100
+ def method_missing(*args, &)
102
101
  # Get the configuration
103
102
  if args.last.is_a?(Hash)
104
103
  options = args.last
@@ -123,13 +122,13 @@ module StateMachines
123
122
  object = condition_args.first || self
124
123
 
125
124
  proxy.evaluate_method(object, proxy_condition) &&
126
- Array(if_condition).all? { |condition| proxy.evaluate_method(object, condition) } &&
127
- !Array(unless_condition).any? { |condition| proxy.evaluate_method(object, condition) }
125
+ Array(if_condition).all? { |condition| proxy.evaluate_method(object, condition) } &&
126
+ !Array(unless_condition).any? { |condition| proxy.evaluate_method(object, condition) }
128
127
  end
129
128
 
130
129
  # Evaluate the method on the owner class with the condition proxied
131
130
  # through
132
- machine.owner_class.send(*args, &block)
131
+ machine.owner_class.send(*args, &)
133
132
  end
134
133
  end
135
134
  end
@@ -13,9 +13,9 @@ module StateMachines
13
13
  end
14
14
 
15
15
  module_function def draw_states(machine:, io: $stdout)
16
- io.puts " States:"
16
+ io.puts ' States:'
17
17
  if machine.states.to_a.empty?
18
- io.puts " - None"
18
+ io.puts ' - None'
19
19
  else
20
20
  machine.states.each do |state|
21
21
  io.puts " - #{state.name}"
@@ -23,31 +23,31 @@ module StateMachines
23
23
  end
24
24
  end
25
25
 
26
- module_function def draw_event(event, graph, options: {}, io: $stdout)
26
+ module_function def draw_event(event, _graph, options: {}, io: $stdout)
27
27
  io = io || options[:io] || $stdout
28
28
  io.puts " Event: #{event.name}"
29
29
  end
30
30
 
31
- module_function def draw_branch(branch, graph, event, options: {}, io: $stdout)
31
+ module_function def draw_branch(branch, _graph, _event, options: {}, io: $stdout)
32
32
  io = io || options[:io] || $stdout
33
33
  io.puts " Branch: #{branch.inspect}"
34
34
  end
35
35
 
36
- module_function def draw_state(state, graph, options: {}, io: $stdout)
36
+ module_function def draw_state(state, _graph, options: {}, io: $stdout)
37
37
  io = io || options[:io] || $stdout
38
38
  io.puts " State: #{state.name}"
39
39
  end
40
40
 
41
41
  module_function def draw_events(machine:, io: $stdout)
42
- io.puts " Events:"
42
+ io.puts ' Events:'
43
43
  if machine.events.to_a.empty?
44
- io.puts " - None"
44
+ io.puts ' - None'
45
45
  else
46
46
  machine.events.each do |event|
47
47
  io.puts " - #{event.name}"
48
48
  event.branches.each do |branch|
49
49
  branch.state_requirements.each do |requirement|
50
- out = +" - "
50
+ out = +' - '
51
51
  out << "#{draw_requirement(requirement[:from])} => #{draw_requirement(requirement[:to])}"
52
52
  out << " IF #{branch.if_condition}" if branch.if_condition
53
53
  out << " UNLESS #{branch.unless_condition}" if branch.unless_condition
@@ -60,14 +60,14 @@ module StateMachines
60
60
 
61
61
  module_function def draw_requirement(requirement)
62
62
  case requirement
63
- when StateMachines::BlacklistMatcher
64
- "ALL EXCEPT #{requirement.values.join(', ')}"
65
- when StateMachines::AllMatcher
66
- "ALL"
67
- when StateMachines::LoopbackMatcher
68
- "SAME"
69
- else
70
- requirement.values.join(', ')
63
+ when StateMachines::BlacklistMatcher
64
+ "ALL EXCEPT #{requirement.values.join(', ')}"
65
+ when StateMachines::AllMatcher
66
+ 'ALL'
67
+ when StateMachines::LoopbackMatcher
68
+ 'SAME'
69
+ else
70
+ requirement.values.join(', ')
71
71
  end
72
72
  end
73
73
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ripper'
4
+
5
+ module StateMachines
6
+ # Cross-platform syntax validation for eval strings
7
+ # Supports CRuby, JRuby, TruffleRuby via pluggable backends
8
+ module SyntaxValidator
9
+ # Public API: raises SyntaxError if code is invalid
10
+ def validate!(code, filename = '(eval)')
11
+ backend.validate!(code, filename)
12
+ end
13
+ module_function :validate!
14
+
15
+ private
16
+
17
+ # Lazily pick the best backend for this platform
18
+ # Prefer RubyVM for performance on CRuby, fallback to Ripper for compatibility
19
+ def backend
20
+ @backend ||= if RubyVmBackend.available?
21
+ RubyVmBackend
22
+ else
23
+ RipperBackend
24
+ end
25
+ end
26
+ module_function :backend
27
+
28
+ # MRI backend using RubyVM::InstructionSequence
29
+ module RubyVmBackend
30
+ def available?
31
+ RUBY_ENGINE == 'ruby'
32
+ end
33
+ module_function :available?
34
+
35
+ def validate!(code, filename)
36
+ # compile will raise a SyntaxError on bad syntax
37
+ RubyVM::InstructionSequence.compile(code, filename)
38
+ true
39
+ end
40
+ module_function :validate!
41
+ end
42
+
43
+ # Universal Ruby backend via Ripper
44
+ module RipperBackend
45
+ def validate!(code, filename)
46
+ sexp = Ripper.sexp(code)
47
+ if sexp.nil?
48
+ # Ripper.sexp returns nil on a parse error, but no exception
49
+ raise SyntaxError, "syntax error in #{filename}"
50
+ end
51
+
52
+ true
53
+ end
54
+ module_function :validate!
55
+ end
56
+ end
57
+ end