state_machines 0.6.0 → 0.20.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +93 -13
  3. data/lib/state_machines/branch.rb +8 -4
  4. data/lib/state_machines/callback.rb +2 -0
  5. data/lib/state_machines/core.rb +3 -2
  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 +2 -0
  9. data/lib/state_machines/eval_helpers.rb +38 -9
  10. data/lib/state_machines/event.rb +22 -7
  11. data/lib/state_machines/event_collection.rb +2 -0
  12. data/lib/state_machines/extensions.rb +2 -0
  13. data/lib/state_machines/helper_module.rb +2 -0
  14. data/lib/state_machines/integrations/base.rb +2 -0
  15. data/lib/state_machines/integrations.rb +2 -0
  16. data/lib/state_machines/machine/class_methods.rb +79 -0
  17. data/lib/state_machines/machine.rb +21 -67
  18. data/lib/state_machines/machine_collection.rb +5 -1
  19. data/lib/state_machines/macro_methods.rb +2 -0
  20. data/lib/state_machines/matcher.rb +3 -0
  21. data/lib/state_machines/matcher_helpers.rb +2 -0
  22. data/lib/state_machines/node_collection.rb +5 -1
  23. data/lib/state_machines/options_validator.rb +72 -0
  24. data/lib/state_machines/path.rb +5 -1
  25. data/lib/state_machines/path_collection.rb +5 -1
  26. data/lib/state_machines/state.rb +71 -43
  27. data/lib/state_machines/state_collection.rb +2 -0
  28. data/lib/state_machines/state_context.rb +5 -1
  29. data/lib/state_machines/stdio_renderer.rb +74 -0
  30. data/lib/state_machines/test_helper.rb +305 -0
  31. data/lib/state_machines/transition.rb +2 -0
  32. data/lib/state_machines/transition_collection.rb +5 -1
  33. data/lib/state_machines/version.rb +3 -1
  34. data/lib/state_machines.rb +4 -1
  35. metadata +11 -9
  36. data/lib/state_machines/assertions.rb +0 -40
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d22dcf0b70eca7cbf9a6894d592609874cfdef01474b70a2226018c0e8966b7
4
- data.tar.gz: 9a9bed680181f45bc613649bf4f3b90a4ea1e198e8fb19f734d264c7652e5051
3
+ metadata.gz: 5aa5d105c78cb53f15f42e8662dd7f8e0d0d5f8807b88dcbd8b4201f13718384
4
+ data.tar.gz: d761cedbe052c5c8829626e0875f54dce064f7156162119b4a040594b439068c
5
5
  SHA512:
6
- metadata.gz: d1446488cbb427e407611d2cba2e571401931a4a20f9f8655e14ebf896990468d24a94a02ba708627db8beb3a3821ac481b220551f0d0c02e366ba3d78112da4
7
- data.tar.gz: 4949561813ac58b6442611d37083091b6c4532f41227f3f9f0999bbecc18c18aacc458f9aa6d6570b5855b6a30a24394d421e6b6012dfa4ebed638a642da82f3
6
+ metadata.gz: 37398c9ca5f2de7413dfb7a8a467984d0fc4d47f8367ea0747fec6d9c1eaca5a7c3439f37988c02dd8ce27622a9a9e54cf5cc0b2d84404f00f92a862c168bb23
7
+ data.tar.gz: 159022534eb3c308bc2f2c8e960f111053631d3e10adafc9ae8156c95a26165f48690c682229a577bdfecf918b335ebe5b6d57e82e095c924585ad8d11b9d1ee
data/README.md CHANGED
@@ -1,5 +1,4 @@
1
1
  ![Build Status](https://github.com/state-machines/state_machines/actions/workflows/ruby.yml/badge.svg)
2
- [![Code Climate](https://codeclimate.com/github/state-machines/state_machines.svg)](https://codeclimate.com/github/state-machines/state_machines)
3
2
  # State Machines
4
3
 
5
4
  State Machines adds support for creating state machines for attributes on any Ruby class.
@@ -255,6 +254,71 @@ vehicle.state_name # => :parked
255
254
  # vehicle.state = :parked
256
255
  ```
257
256
 
257
+ ## Testing
258
+
259
+ State Machines provides a `TestHelper` module with assertion methods to make testing state machines easier and more expressive.
260
+
261
+ ### Setup
262
+
263
+ Include the test helper in your test class:
264
+
265
+ ```ruby
266
+ # For Minitest
267
+ class VehicleTest < Minitest::Test
268
+ include StateMachines::TestHelper
269
+
270
+ def test_initial_state
271
+ vehicle = Vehicle.new
272
+ assert_state vehicle, :state, :parked
273
+ end
274
+ end
275
+
276
+ # For RSpec
277
+ RSpec.describe Vehicle do
278
+ include StateMachines::TestHelper
279
+
280
+ it "starts in parked state" do
281
+ vehicle = Vehicle.new
282
+ assert_state vehicle, :state, :parked
283
+ end
284
+ end
285
+ ```
286
+
287
+ ### Available Assertions
288
+
289
+ The TestHelper provides both basic assertions and comprehensive state machine-specific assertions with `sm_` prefixes:
290
+
291
+ #### Basic Assertions
292
+
293
+ ```ruby
294
+ vehicle = Vehicle.new
295
+ assert_state vehicle, :state, :parked
296
+ assert_can_transition vehicle, :ignite
297
+ assert_cannot_transition vehicle, :shift_up
298
+ assert_transition vehicle, :ignite, :state, :idling
299
+ ```
300
+
301
+ #### Extended State Machine Assertions
302
+
303
+ ```ruby
304
+ machine = Vehicle.state_machine(:state)
305
+ vehicle = Vehicle.new
306
+
307
+ # State configuration
308
+ assert_sm_states_list machine, [:parked, :idling, :stalled]
309
+ assert_sm_initial_state machine, :parked
310
+
311
+ # Event behavior
312
+ assert_sm_event_triggers vehicle, :ignite
313
+ refute_sm_event_triggers vehicle, :shift_up
314
+ assert_sm_event_raises_error vehicle, :invalid_event, StateMachines::InvalidTransition
315
+
316
+ # Persistence (with ActiveRecord integration)
317
+ assert_sm_state_persisted record, expected: :active
318
+ ```
319
+
320
+ The test helper works with both Minitest and RSpec, automatically detecting your testing framework.
321
+
258
322
  ## Additional Topics
259
323
 
260
324
  ### Explicit vs. Implicit Event Transitions
@@ -428,7 +492,7 @@ easily migrate from a different library, you can do so as shown below:
428
492
  ```ruby
429
493
  class Vehicle
430
494
  state_machine initial: :parked do
431
- ...
495
+ # ...
432
496
 
433
497
  state :parked do
434
498
  transition to: :idling, :on => [:ignite, :shift_up], if: :seatbelt_on?
@@ -464,7 +528,7 @@ example below:
464
528
  ```ruby
465
529
  class Vehicle
466
530
  state_machine initial: :parked do
467
- ...
531
+ # ...
468
532
 
469
533
  transition parked: :idling, :on => [:ignite, :shift_up]
470
534
  transition first_gear: :second_gear, second_gear: :third_gear, on: :shift_up
@@ -496,12 +560,31 @@ class Vehicle
496
560
  transition [:idling, :first_gear] => :parked
497
561
  end
498
562
 
499
- ...
563
+ # ...
500
564
  end
501
565
  end
502
566
  ```
503
567
 
504
- However, there may be cases where the definition of a state machine is **dynamic**.
568
+ #### Draw state machines
569
+
570
+ State machines includes a default STDIORenderer for debugging state machines without external dependencies.
571
+ This renderer can be used to visualize the state machine in the console.
572
+
573
+ To use the renderer, simply call the `draw` method on the state machine:
574
+
575
+ ```ruby
576
+ Vehicle.state_machine.draw # Outputs the state machine diagram to the console
577
+ ```
578
+
579
+ You can customize the output by passing in options to the `draw` method, such as the output stream:
580
+
581
+ ```ruby
582
+ Vehicle.state_machine.draw(io: $stderr) # Outputs the state machine diagram to stderr
583
+ ```
584
+
585
+ #### Dynamic definitions
586
+
587
+ There may be cases where the definition of a state machine is **dynamic**.
505
588
  This means that you don't know the possible states or events for a machine until
506
589
  runtime. For example, you may allow users in your application to manage the
507
590
  state machine of a project or task in your system. This means that the list of
@@ -580,22 +663,19 @@ transitions.
580
663
 
581
664
  Ruby versions officially supported and tested:
582
665
 
583
- * Ruby (MRI) 2.6.0+
584
- * JRuby
585
- * Rubinius
666
+ * Ruby (MRI) 3.0.0+
586
667
 
587
668
  For graphing state machine:
588
669
 
589
- * [state_machines-graphviz](http://github.com/state-machines/state_machines-graphviz)
670
+ * [state_machines-graphviz](https://github.com/state-machines/state_machines-graphviz)
590
671
 
591
672
  For documenting state machines:
592
673
 
593
- * [state_machines-yard](http://github.com/state-machines/state_machines-yard)
594
-
674
+ * [state_machines-yard](https://github.com/state-machines/state_machines-yard)
595
675
 
596
- ## TODO
676
+ For RSpec testing, use the custom RSpec matchers:
597
677
 
598
- * Add matchers/assertions for rspec and minitest
678
+ * [state_machines-rspec](https://github.com/state-machines/state_machines-rspec)
599
679
 
600
680
  ## Contributing
601
681
 
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options_validator'
4
+
1
5
  module StateMachines
2
6
  # Represents a set of requirements that must be met in order for a transition
3
7
  # or callback to occur. Branches verify that the event, from state, and to
@@ -112,15 +116,15 @@ module StateMachines
112
116
  # branch.match(object, :on => :ignite) # => {:to => ..., :from => ..., :on => ...}
113
117
  # branch.match(object, :on => :park) # => nil
114
118
  def match(object, query = {})
115
- query.assert_valid_keys(:from, :to, :on, :guard)
119
+ StateMachines::OptionsValidator.assert_valid_keys!(query, :from, :to, :on, :guard)
116
120
 
117
121
  if (match = match_query(query)) && matches_conditions?(object, query)
118
122
  match
119
123
  end
120
124
  end
121
125
 
122
- def draw(graph, event, valid_states)
123
- fail NotImplementedError
126
+ def draw(graph, event, valid_states, io = $stdout)
127
+ machine.renderer.draw_branch(self, graph, event, valid_states, io)
124
128
  end
125
129
 
126
130
  protected
@@ -129,7 +133,7 @@ module StateMachines
129
133
  # whitelist nor a blacklist option is specified, then an AllMatcher is
130
134
  # built.
131
135
  def build_matcher(options, whitelist_option, blacklist_option)
132
- options.assert_exclusive_keys(whitelist_option, blacklist_option)
136
+ StateMachines::OptionsValidator.assert_exclusive_keys!(options, whitelist_option, blacklist_option)
133
137
 
134
138
  if options.include?(whitelist_option)
135
139
  value = options[whitelist_option]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'state_machines/branch'
2
4
  require 'state_machines/eval_helpers'
3
5
 
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Load all of the core implementation required to use state_machine. This
2
4
  # includes:
3
5
  # * StateMachines::MacroMethods which adds the state_machine DSL to your class
4
6
  # * A set of initializers for setting state_machine defaults based on the current
5
7
  # running environment (such as within Rails)
6
- require 'state_machines/assertions'
7
8
  require 'state_machines/error'
8
9
 
9
10
  require 'state_machines/extensions'
@@ -40,4 +41,4 @@ require 'state_machines/path_collection'
40
41
  require 'state_machines/machine'
41
42
  require 'state_machines/machine_collection'
42
43
 
43
- require 'state_machines/macro_methods'
44
+ require 'state_machines/macro_methods'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Class.class_eval do
2
4
  include StateMachines::MacroMethods
3
5
  end
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Loads all of the extensions to be made to Ruby core classes
2
4
  require 'state_machines/core_ext/class/state_machine'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # An error occurred during a state machine invocation
3
5
  class Error < StandardError
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # Provides a set of helper methods for evaluating methods within the context
3
5
  # of an object.
@@ -50,19 +52,17 @@ module StateMachines
50
52
  # evaluate_method(person, lambda {|person| person.name}, 21) # => "John Smith"
51
53
  # evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21) # => "John Smith is 21"
52
54
  # evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21, 'male') # => ArgumentError: wrong number of arguments (3 for 2)
53
- def evaluate_method(object, method, *args, &block)
55
+ def evaluate_method(object, method, *args, **kwargs, &block)
54
56
  case method
55
57
  when Symbol
56
58
  klass = (class << object; self; end)
57
59
  args = [] if (klass.method_defined?(method) || klass.private_method_defined?(method)) && object.method(method).arity == 0
58
- object.send(method, *args, &block)
59
- when Proc, Method
60
+ object.send(method, *args, **kwargs, &block)
61
+ when Proc
60
62
  args.unshift(object)
61
63
  arity = method.arity
62
-
63
- # Procs don't support blocks in < Ruby 1.9, so it's tacked on as an
64
- # argument for consistency across versions of Ruby
65
- if block_given? && Proc === method && arity != 0
64
+ # Handle blocks for Procs
65
+ if block_given? && arity != 0
66
66
  if [1, 2].include?(arity)
67
67
  # Force the block to be either the only argument or the 2nd one
68
68
  # after the object (may mean additional arguments get discarded)
@@ -76,9 +76,38 @@ module StateMachines
76
76
  args = args[0, arity] if [0, 1].include?(arity)
77
77
  end
78
78
 
79
- method.is_a?(Proc) ? method.call(*args) : method.call(*args, &block)
79
+ # Call the Proc with the arguments
80
+ method.call(*args, **kwargs)
81
+
82
+ when Method
83
+ args.unshift(object)
84
+ arity = method.arity
85
+
86
+ # Methods handle blocks via &block, not as arguments
87
+ # Only limit arguments if necessary based on arity
88
+ args = args[0, arity] if [0, 1].include?(arity)
89
+
90
+ # Call the Method with the arguments and pass the block
91
+ method.call(*args, **kwargs, &block)
80
92
  when String
81
- eval(method, object.instance_eval { binding }, &block)
93
+ if block_given?
94
+ if StateMachines::Transition.pause_supported?
95
+ eval(method, object.instance_eval { binding }, &block)
96
+ else
97
+ # Support for JRuby and Truffle Ruby, which don't support binding blocks
98
+ eigen = class << object; self; end
99
+ eigen.class_eval <<-RUBY, __FILE__, __LINE__ + 1
100
+ def __temp_eval_method__(*args, &b)
101
+ #{method}
102
+ end
103
+ RUBY
104
+ result = object.__temp_eval_method__(*args, &block)
105
+ eigen.send(:remove_method, :__temp_eval_method__)
106
+ result
107
+ end
108
+ else
109
+ eval(method, object.instance_eval { binding })
110
+ end
82
111
  else
83
112
  raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated'
84
113
  end
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options_validator'
4
+
1
5
  module StateMachines
2
6
  # An event defines an action that transitions an attribute from one state to
3
7
  # another. The state that an attribute is transitioned to depends on the
@@ -30,13 +34,22 @@ module StateMachines
30
34
  #
31
35
  # Configuration options:
32
36
  # * <tt>:human_name</tt> - The human-readable version of this event's name
33
- def initialize(machine, name, options = {}) #:nodoc:
34
- options.assert_valid_keys(:human_name)
37
+ def initialize(machine, name, options = nil, human_name: nil, **extra_options) #:nodoc:
38
+ # Handle both old hash style and new kwargs style for backward compatibility
39
+ if options.is_a?(Hash)
40
+ # Old style: initialize(machine, name, {human_name: 'Custom Name'})
41
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :human_name)
42
+ human_name = options[:human_name]
43
+ else
44
+ # New style: initialize(machine, name, human_name: 'Custom Name')
45
+ raise ArgumentError, "Unexpected positional argument: #{options.inspect}" unless options.nil?
46
+ StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :human_name) unless extra_options.empty?
47
+ end
35
48
 
36
49
  @machine = machine
37
50
  @name = name
38
51
  @qualified_name = machine.namespace ? :"#{name}_#{machine.namespace}" : name
39
- @human_name = options[:human_name] || @name.to_s.tr('_', ' ')
52
+ @human_name = human_name || @name.to_s.tr('_', ' ')
40
53
  reset
41
54
 
42
55
  # Output a warning if another event has a conflicting qualified name
@@ -89,7 +102,9 @@ module StateMachines
89
102
 
90
103
  # Only a certain subset of explicit options are allowed for transition
91
104
  # requirements
92
- options.assert_valid_keys(:from, :to, :except_from, :except_to, :if, :unless) if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).empty?
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
93
108
 
94
109
  branches << branch = Branch.new(options.merge(on: name))
95
110
  @known_states |= branch.known_states
@@ -119,7 +134,7 @@ module StateMachines
119
134
  # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
120
135
  # conditionals defined for each one. Default is true.
121
136
  def transition_for(object, requirements = {})
122
- requirements.assert_valid_keys(:from, :to, :guard)
137
+ StateMachines::OptionsValidator.assert_valid_keys!(requirements, :from, :to, :guard)
123
138
  requirements[:from] = machine.states.match!(object).name unless (custom_from_state = requirements.include?(:from))
124
139
 
125
140
  branches.each do |branch|
@@ -180,8 +195,8 @@ module StateMachines
180
195
  end
181
196
 
182
197
 
183
- def draw(graph, options = {})
184
- fail NotImplementedError
198
+ def draw(graph, options = {}, io = $stdout)
199
+ machine.renderer.draw_event(self, graph, options, io)
185
200
  end
186
201
 
187
202
  # Generates a nicely formatted description of this event's contents.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # Represents a collection of events in a state machine
3
5
  class EventCollection < NodeCollection
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  module ClassMethods
3
5
  def self.extended(base) #:nodoc:
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # Represents a type of module that defines instance / class methods for a
3
5
  # state machine
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  module Integrations
3
5
  # Provides a set of base helpers for managing individual integrations
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # Integrations allow state machines to take advantage of features within the
3
5
  # context of a particular library. This is currently most useful with
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ class Machine
5
+ module ClassMethods
6
+ # Attempts to find or create a state machine for the given class. For
7
+ # example,
8
+ #
9
+ # StateMachines::Machine.find_or_create(Vehicle)
10
+ # StateMachines::Machine.find_or_create(Vehicle, :initial => :parked)
11
+ # StateMachines::Machine.find_or_create(Vehicle, :status)
12
+ # StateMachines::Machine.find_or_create(Vehicle, :status, :initial => :parked)
13
+ #
14
+ # If a machine of the given name already exists in one of the class's
15
+ # superclasses, then a copy of that machine will be created and stored
16
+ # in the new owner class (the original will remain unchanged).
17
+ def find_or_create(owner_class, *args, &block)
18
+ options = args.last.is_a?(Hash) ? args.pop : {}
19
+ name = args.first || :state
20
+
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
25
+
26
+ if machine
27
+ # Only create a new copy if changes are being made to the machine in
28
+ # a subclass
29
+ if machine.owner_class != owner_class && (options.any? || block_given?)
30
+ machine = machine.clone
31
+ machine.initial_state = options[:initial] if options.include?(:initial)
32
+ machine.owner_class = owner_class
33
+ end
34
+
35
+ # Evaluate DSL
36
+ machine.instance_eval(&block) if block_given?
37
+ else
38
+ # No existing machine: create a new one
39
+ machine = new(owner_class, name, options, &block)
40
+ end
41
+
42
+ machine
43
+ end
44
+
45
+ def draw(*)
46
+ raise NotImplementedError
47
+ end
48
+
49
+ # Default messages to use for validation errors in ORM integrations
50
+ attr_accessor :ignore_method_conflicts
51
+
52
+ def default_messages
53
+ @default_messages ||= {
54
+ invalid: 'is invalid',
55
+ invalid_event: 'cannot transition when %s',
56
+ invalid_transition: 'cannot transition via "%1$s"'
57
+ }
58
+ end
59
+
60
+ def default_messages=(messages)
61
+ @default_messages = messages
62
+ end
63
+
64
+ def replace_messages(message_hash)
65
+ message_hash.each do |key, value|
66
+ default_messages[key] = value
67
+ end
68
+ end
69
+
70
+ attr_writer :renderer
71
+
72
+ def renderer
73
+ return @renderer if @renderer
74
+
75
+ STDIORenderer
76
+ end
77
+ end
78
+ end
79
+ end
@@ -1,3 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options_validator'
4
+ require_relative 'machine/class_methods'
5
+
1
6
  module StateMachines
2
7
  # Represents a state machine for a particular attribute. State machines
3
8
  # consist of states, events and a set of transitions that define how the
@@ -398,65 +403,10 @@ module StateMachines
398
403
  # machine's behavior, refer to all constants defined under the
399
404
  # StateMachines::Integrations namespace.
400
405
  class Machine
401
-
406
+ extend ClassMethods
402
407
  include EvalHelpers
403
408
  include MatcherHelpers
404
409
 
405
- class << self
406
- # Attempts to find or create a state machine for the given class. For
407
- # example,
408
- #
409
- # StateMachines::Machine.find_or_create(Vehicle)
410
- # StateMachines::Machine.find_or_create(Vehicle, :initial => :parked)
411
- # StateMachines::Machine.find_or_create(Vehicle, :status)
412
- # StateMachines::Machine.find_or_create(Vehicle, :status, :initial => :parked)
413
- #
414
- # If a machine of the given name already exists in one of the class's
415
- # superclasses, then a copy of that machine will be created and stored
416
- # in the new owner class (the original will remain unchanged).
417
- def find_or_create(owner_class, *args, &block)
418
- options = args.last.is_a?(Hash) ? args.pop : {}
419
- name = args.first || :state
420
-
421
- # Find an existing machine
422
- machine = owner_class.respond_to?(:state_machines) &&
423
- (args.first && owner_class.state_machines[name] || !args.first &&
424
- owner_class.state_machines.values.first) || nil
425
-
426
- if machine
427
- # Only create a new copy if changes are being made to the machine in
428
- # a subclass
429
- if machine.owner_class != owner_class && (options.any? || block_given?)
430
- machine = machine.clone
431
- machine.initial_state = options[:initial] if options.include?(:initial)
432
- machine.owner_class = owner_class
433
- end
434
-
435
- # Evaluate DSL
436
- machine.instance_eval(&block) if block_given?
437
- else
438
- # No existing machine: create a new one
439
- machine = new(owner_class, name, options, &block)
440
- end
441
-
442
- machine
443
- end
444
-
445
-
446
- def draw(*)
447
- fail NotImplementedError
448
- end
449
-
450
- # Default messages to use for validation errors in ORM integrations
451
- attr_accessor :default_messages
452
- attr_accessor :ignore_method_conflicts
453
- end
454
- @default_messages = {
455
- invalid: 'is invalid',
456
- invalid_event: 'cannot transition when %s',
457
- invalid_transition: 'cannot transition via "%1$s"'
458
- }
459
-
460
410
  # Whether to ignore any conflicts that are detected for helper methods that
461
411
  # get generated for a machine's owner class. Default is false.
462
412
  @ignore_method_conflicts = false
@@ -502,7 +452,7 @@ module StateMachines
502
452
  # Creates a new state machine for the given attribute
503
453
  def initialize(owner_class, *args, &block)
504
454
  options = args.last.is_a?(Hash) ? args.pop : {}
505
- options.assert_valid_keys(:attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
455
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
506
456
 
507
457
  # Find an integration that matches this machine's owner class
508
458
  if options.include?(:integration)
@@ -644,12 +594,12 @@ module StateMachines
644
594
  # vehicle.force_idle = false
645
595
  # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=false>
646
596
  def initial_state(object)
647
- states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?('@initial_state')
597
+ states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?(:@initial_state)
648
598
  end
649
599
 
650
600
  # Whether a dynamic initial state is being used in the machine
651
601
  def dynamic_initial_state?
652
- instance_variable_defined?('@initial_state') && @initial_state.is_a?(Proc)
602
+ instance_variable_defined?(:@initial_state) && @initial_state.is_a?(Proc)
653
603
  end
654
604
 
655
605
  # Initializes the state on the given object. Initial values are only set if
@@ -1004,7 +954,7 @@ module StateMachines
1004
954
  # options hash which contains at least <tt>:if</tt> condition support.
1005
955
  def state(*names, &block)
1006
956
  options = names.last.is_a?(Hash) ? names.pop : {}
1007
- options.assert_valid_keys(:value, :cache, :if, :human_name)
957
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :value, :cache, :if, :human_name)
1008
958
 
1009
959
  # Store the context so that it can be used for / matched against any state
1010
960
  # that gets added
@@ -1053,7 +1003,7 @@ module StateMachines
1053
1003
  def read(object, attribute, ivar = false)
1054
1004
  attribute = self.attribute(attribute)
1055
1005
  if ivar
1056
- object.instance_variable_defined?("@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
1006
+ object.instance_variable_defined?(:"@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
1057
1007
  else
1058
1008
  object.send(attribute)
1059
1009
  end
@@ -1076,7 +1026,7 @@ module StateMachines
1076
1026
  # vehicle.event # => "park"
1077
1027
  def write(object, attribute, value, ivar = false)
1078
1028
  attribute = self.attribute(attribute)
1079
- ivar ? object.instance_variable_set("@#{attribute}", value) : object.send("#{attribute}=", value)
1029
+ ivar ? object.instance_variable_set(:"@#{attribute}", value) : object.send("#{attribute}=", value)
1080
1030
  end
1081
1031
 
1082
1032
  # Defines one or more events for the machine and the transitions that can
@@ -1307,7 +1257,7 @@ module StateMachines
1307
1257
  # end
1308
1258
  def event(*names, &block)
1309
1259
  options = names.last.is_a?(Hash) ? names.pop : {}
1310
- options.assert_valid_keys(:human_name)
1260
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :human_name)
1311
1261
 
1312
1262
  # Store the context so that it can be used for / matched against any event
1313
1263
  # that gets added
@@ -1749,7 +1699,7 @@ module StateMachines
1749
1699
  def after_failure(*args, &block)
1750
1700
  options = (args.last.is_a?(Hash) ? args.pop : {})
1751
1701
  options[:do] = args if args.any?
1752
- options.assert_valid_keys(:on, :do, :if, :unless)
1702
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :on, :do, :if, :unless)
1753
1703
 
1754
1704
  add_callback(:failure, options, &block)
1755
1705
  end
@@ -1874,8 +1824,12 @@ module StateMachines
1874
1824
  end
1875
1825
 
1876
1826
 
1877
- def draw(*)
1878
- fail NotImplementedError
1827
+ def renderer
1828
+ self.class.renderer
1829
+ end
1830
+
1831
+ def draw(**options)
1832
+ renderer.draw_machine(self, **options)
1879
1833
  end
1880
1834
 
1881
1835
  # Determines whether an action hook was defined for firing attribute-based
@@ -1884,7 +1838,7 @@ module StateMachines
1884
1838
  @action_hook_defined || !self_only && owner_class.state_machines.any? { |name, machine| machine.action == action && machine != self && machine.action_hook?(true) }
1885
1839
  end
1886
1840
 
1887
- protected
1841
+ protected
1888
1842
 
1889
1843
  # Runs additional initialization hooks. By default, this is a no-op.
1890
1844
  def after_initialize