state_machines 0.6.0 → 0.10.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +28 -12
  3. data/lib/state_machines/assertions.rb +2 -0
  4. data/lib/state_machines/branch.rb +4 -2
  5. data/lib/state_machines/callback.rb +2 -0
  6. data/lib/state_machines/core.rb +2 -0
  7. data/lib/state_machines/core_ext/class/state_machine.rb +2 -0
  8. data/lib/state_machines/core_ext.rb +2 -0
  9. data/lib/state_machines/error.rb +2 -0
  10. data/lib/state_machines/eval_helpers.rb +38 -9
  11. data/lib/state_machines/event.rb +4 -2
  12. data/lib/state_machines/event_collection.rb +2 -0
  13. data/lib/state_machines/extensions.rb +2 -0
  14. data/lib/state_machines/helper_module.rb +2 -0
  15. data/lib/state_machines/integrations/base.rb +2 -0
  16. data/lib/state_machines/integrations.rb +2 -0
  17. data/lib/state_machines/machine/class_methods.rb +79 -0
  18. data/lib/state_machines/machine.rb +16 -63
  19. data/lib/state_machines/machine_collection.rb +2 -0
  20. data/lib/state_machines/macro_methods.rb +2 -0
  21. data/lib/state_machines/matcher.rb +3 -0
  22. data/lib/state_machines/matcher_helpers.rb +2 -0
  23. data/lib/state_machines/node_collection.rb +2 -0
  24. data/lib/state_machines/path.rb +2 -0
  25. data/lib/state_machines/path_collection.rb +2 -0
  26. data/lib/state_machines/state.rb +48 -37
  27. data/lib/state_machines/state_collection.rb +2 -0
  28. data/lib/state_machines/state_context.rb +2 -0
  29. data/lib/state_machines/stdio_renderer.rb +74 -0
  30. data/lib/state_machines/transition.rb +2 -0
  31. data/lib/state_machines/transition_collection.rb +2 -0
  32. data/lib/state_machines/version.rb +3 -1
  33. data/lib/state_machines.rb +4 -1
  34. metadata +9 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d22dcf0b70eca7cbf9a6894d592609874cfdef01474b70a2226018c0e8966b7
4
- data.tar.gz: 9a9bed680181f45bc613649bf4f3b90a4ea1e198e8fb19f734d264c7652e5051
3
+ metadata.gz: 39c8073bdf33aff2e34bfc3403db686c7d6e4344f158e144cb881230b10d9597
4
+ data.tar.gz: 150e14164e42addded79421e90456de2f60b4c451c743ee31a8eb2227d91896b
5
5
  SHA512:
6
- metadata.gz: d1446488cbb427e407611d2cba2e571401931a4a20f9f8655e14ebf896990468d24a94a02ba708627db8beb3a3821ac481b220551f0d0c02e366ba3d78112da4
7
- data.tar.gz: 4949561813ac58b6442611d37083091b6c4532f41227f3f9f0999bbecc18c18aacc458f9aa6d6570b5855b6a30a24394d421e6b6012dfa4ebed638a642da82f3
6
+ metadata.gz: 34af008f9378c058a199dd05d3204df5d995466f7db126589a8614d540bb636ce08c42f38796504753be8414431623e02b30c10d772fc2d524d4a2f9f4ba58e2
7
+ data.tar.gz: a64f178f0f5ce8e13139b871c3cd351570df7c665f43338201225ff27d2482fe3cb3e1619f79e01544d4f9b3341bb9e589db7a659717e4975312684b6fd0ee82
data/README.md CHANGED
@@ -428,7 +428,7 @@ easily migrate from a different library, you can do so as shown below:
428
428
  ```ruby
429
429
  class Vehicle
430
430
  state_machine initial: :parked do
431
- ...
431
+ # ...
432
432
 
433
433
  state :parked do
434
434
  transition to: :idling, :on => [:ignite, :shift_up], if: :seatbelt_on?
@@ -464,7 +464,7 @@ example below:
464
464
  ```ruby
465
465
  class Vehicle
466
466
  state_machine initial: :parked do
467
- ...
467
+ # ...
468
468
 
469
469
  transition parked: :idling, :on => [:ignite, :shift_up]
470
470
  transition first_gear: :second_gear, second_gear: :third_gear, on: :shift_up
@@ -496,12 +496,31 @@ class Vehicle
496
496
  transition [:idling, :first_gear] => :parked
497
497
  end
498
498
 
499
- ...
499
+ # ...
500
500
  end
501
501
  end
502
502
  ```
503
503
 
504
- However, there may be cases where the definition of a state machine is **dynamic**.
504
+ #### Draw state machines
505
+
506
+ State machines includes a default STDIORenderer for debugging state machines without external dependencies.
507
+ This renderer can be used to visualize the state machine in the console.
508
+
509
+ To use the renderer, simply call the `draw` method on the state machine:
510
+
511
+ ```ruby
512
+ Vehicle.state_machine.draw # Outputs the state machine diagram to the console
513
+ ```
514
+
515
+ You can customize the output by passing in options to the `draw` method, such as the output stream:
516
+
517
+ ```ruby
518
+ Vehicle.state_machine.draw(io: $stderr) # Outputs the state machine diagram to stderr
519
+ ```
520
+
521
+ #### Dynamic definitions
522
+
523
+ There may be cases where the definition of a state machine is **dynamic**.
505
524
  This means that you don't know the possible states or events for a machine until
506
525
  runtime. For example, you may allow users in your application to manage the
507
526
  state machine of a project or task in your system. This means that the list of
@@ -580,22 +599,19 @@ transitions.
580
599
 
581
600
  Ruby versions officially supported and tested:
582
601
 
583
- * Ruby (MRI) 2.6.0+
584
- * JRuby
585
- * Rubinius
602
+ * Ruby (MRI) 3.0.0+
586
603
 
587
604
  For graphing state machine:
588
605
 
589
- * [state_machines-graphviz](http://github.com/state-machines/state_machines-graphviz)
606
+ * [state_machines-graphviz](https://github.com/state-machines/state_machines-graphviz)
590
607
 
591
608
  For documenting state machines:
592
609
 
593
- * [state_machines-yard](http://github.com/state-machines/state_machines-yard)
594
-
610
+ * [state_machines-yard](https://github.com/state-machines/state_machines-yard)
595
611
 
596
- ## TODO
612
+ For RSpec testing, use the custom RSpec matchers:
597
613
 
598
- * Add matchers/assertions for rspec and minitest
614
+ * [state_machines-rspec](https://github.com/state-machines/state_machines-rspec)
599
615
 
600
616
  ## Contributing
601
617
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Hash
2
4
  # Provides a set of helper methods for making assertions about the content
3
5
  # of various objects
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # Represents a set of requirements that must be met in order for a transition
3
5
  # or callback to occur. Branches verify that the event, from state, and to
@@ -119,8 +121,8 @@ module StateMachines
119
121
  end
120
122
  end
121
123
 
122
- def draw(graph, event, valid_states)
123
- fail NotImplementedError
124
+ def draw(graph, event, valid_states, io = $stdout)
125
+ machine.renderer.draw_branch(self, graph, event, valid_states, io)
124
126
  end
125
127
 
126
128
  protected
@@ -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,3 +1,5 @@
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
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # An event defines an action that transitions an attribute from one state to
3
5
  # another. The state that an attribute is transitioned to depends on the
@@ -180,8 +182,8 @@ module StateMachines
180
182
  end
181
183
 
182
184
 
183
- def draw(graph, options = {})
184
- fail NotImplementedError
185
+ def draw(graph, options = {}, io = $stdout)
186
+ machine.renderer.draw_event(self, graph, options, io)
185
187
  end
186
188
 
187
189
  # 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'machine/class_methods'
4
+
1
5
  module StateMachines
2
6
  # Represents a state machine for a particular attribute. State machines
3
7
  # consist of states, events and a set of transitions that define how the
@@ -398,65 +402,10 @@ module StateMachines
398
402
  # machine's behavior, refer to all constants defined under the
399
403
  # StateMachines::Integrations namespace.
400
404
  class Machine
401
-
405
+ extend ClassMethods
402
406
  include EvalHelpers
403
407
  include MatcherHelpers
404
408
 
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
409
  # Whether to ignore any conflicts that are detected for helper methods that
461
410
  # get generated for a machine's owner class. Default is false.
462
411
  @ignore_method_conflicts = false
@@ -644,12 +593,12 @@ module StateMachines
644
593
  # vehicle.force_idle = false
645
594
  # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=false>
646
595
  def initial_state(object)
647
- states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?('@initial_state')
596
+ states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?(:@initial_state)
648
597
  end
649
598
 
650
599
  # Whether a dynamic initial state is being used in the machine
651
600
  def dynamic_initial_state?
652
- instance_variable_defined?('@initial_state') && @initial_state.is_a?(Proc)
601
+ instance_variable_defined?(:@initial_state) && @initial_state.is_a?(Proc)
653
602
  end
654
603
 
655
604
  # Initializes the state on the given object. Initial values are only set if
@@ -1053,7 +1002,7 @@ module StateMachines
1053
1002
  def read(object, attribute, ivar = false)
1054
1003
  attribute = self.attribute(attribute)
1055
1004
  if ivar
1056
- object.instance_variable_defined?("@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
1005
+ object.instance_variable_defined?(:"@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
1057
1006
  else
1058
1007
  object.send(attribute)
1059
1008
  end
@@ -1076,7 +1025,7 @@ module StateMachines
1076
1025
  # vehicle.event # => "park"
1077
1026
  def write(object, attribute, value, ivar = false)
1078
1027
  attribute = self.attribute(attribute)
1079
- ivar ? object.instance_variable_set("@#{attribute}", value) : object.send("#{attribute}=", value)
1028
+ ivar ? object.instance_variable_set(:"@#{attribute}", value) : object.send("#{attribute}=", value)
1080
1029
  end
1081
1030
 
1082
1031
  # Defines one or more events for the machine and the transitions that can
@@ -1874,8 +1823,12 @@ module StateMachines
1874
1823
  end
1875
1824
 
1876
1825
 
1877
- def draw(*)
1878
- fail NotImplementedError
1826
+ def renderer
1827
+ self.class.renderer
1828
+ end
1829
+
1830
+ def draw(**options)
1831
+ renderer.draw_machine(self, **options)
1879
1832
  end
1880
1833
 
1881
1834
  # Determines whether an action hook was defined for firing attribute-based
@@ -1884,7 +1837,7 @@ module StateMachines
1884
1837
  @action_hook_defined || !self_only && owner_class.state_machines.any? { |name, machine| machine.action == action && machine != self && machine.action_hook?(true) }
1885
1838
  end
1886
1839
 
1887
- protected
1840
+ protected
1888
1841
 
1889
1842
  # Runs additional initialization hooks. By default, this is a no-op.
1890
1843
  def after_initialize
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # Represents a collection of state machines for a class
3
5
  class MachineCollection < Hash
@@ -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.
@@ -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
@@ -33,6 +35,7 @@ module StateMachines
33
35
  def -(blacklist)
34
36
  BlacklistMatcher.new(blacklist)
35
37
  end
38
+ alias_method :except, :-
36
39
 
37
40
  # Always returns true
38
41
  def matches?(value, context = {})
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # Represents a collection of nodes in a state machine, be it events or states.
3
5
  # Nodes will not differentiate between the String and Symbol versions of the
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # A path represents a sequence of transitions that can be run for a particular
3
5
  # object. Paths can walk to new transitions, revealing all of the possible
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # Represents a collection of paths that are generated based on a set of
3
5
  # requirements regarding what states to start and end on
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # A state defines a value that an attribute can be in after being transitioned
3
5
  # 0 or more times. States can represent a value of any type in Ruby, though
@@ -8,7 +10,6 @@ module StateMachines
8
10
  # StateMachines::Machine#state for more information about how state-driven
9
11
  # behavior can be utilized.
10
12
  class State
11
-
12
13
  # The state machine for which this state is defined
13
14
  attr_reader :machine
14
15
 
@@ -31,7 +32,7 @@ module StateMachines
31
32
 
32
33
  # Whether or not this state is the initial state to use for new objects
33
34
  attr_accessor :initial
34
- alias_method :initial?, :initial
35
+ alias initial? initial
35
36
 
36
37
  # A custom lambda block for determining whether a given value matches this
37
38
  # state
@@ -50,7 +51,7 @@ module StateMachines
50
51
  # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
51
52
  # By default, the configured value is matched.
52
53
  # * <tt>:human_name</tt> - The human-readable version of this state's name
53
- def initialize(machine, name, options = {}) #:nodoc:
54
+ def initialize(machine, name, options = {}) # :nodoc:
54
55
  options.assert_valid_keys(:initial, :value, :cache, :if, :human_name)
55
56
 
56
57
  @machine = machine
@@ -63,25 +64,29 @@ module StateMachines
63
64
  @initial = options[:initial] == true
64
65
  @context = StateContext.new(self)
65
66
 
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
67
+ return unless name
68
+
69
+ conflicting_machines = machine.owner_class.state_machines.select do |_other_name, other_machine|
70
+ other_machine != machine && other_machine.states[qualified_name, :qualified_name]
71
+ end
72
+
73
+ # Output a warning if another machine has a conflicting qualified name
74
+ # for a different attribute
75
+ if (conflict = conflicting_machines.detect do |_other_name, other_machine|
76
+ other_machine.attribute != machine.attribute
77
+ end)
78
+ _name, other_machine = conflict
79
+ warn "State #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
80
+ elsif conflicting_machines.empty?
81
+ # Only bother adding predicates when another machine for the same
82
+ # attribute hasn't already done so
83
+ add_predicate
79
84
  end
80
85
  end
81
86
 
82
87
  # Creates a copy of this state, excluding the context to prevent conflicts
83
88
  # across different machines.
84
- def initialize_copy(orig) #:nodoc:
89
+ def initialize_copy(orig) # :nodoc:
85
90
  super
86
91
  @context = StateContext.new(self)
87
92
  end
@@ -96,7 +101,7 @@ module StateMachines
96
101
  # Any objects in a final state will remain so forever given the current
97
102
  # machine's definition.
98
103
  def final?
99
- !machine.events.any? do |event|
104
+ machine.events.none? do |event|
100
105
  event.branches.any? do |branch|
101
106
  branch.state_requirements.any? do |requirement|
102
107
  requirement[:from].matches?(name) && !requirement[:to].matches?(name, from: name)
@@ -126,7 +131,7 @@ module StateMachines
126
131
  # description or just the internal name
127
132
  def description(options = {})
128
133
  label = options[:human_name] ? human_name : name
129
- description = label ? label.to_s : label.inspect
134
+ description = +(label ? label.to_s : label.inspect)
130
135
  description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s
131
136
  description
132
137
  end
@@ -186,19 +191,19 @@ module StateMachines
186
191
  # Evaluate the method definitions and track which ones were added
187
192
  old_methods = context_methods
188
193
  context.class_eval(&block)
189
- new_methods = context_methods.to_a.select { |(name, method)| old_methods[name] != method }
194
+ new_methods = context_methods.to_a.reject { |(name, method)| old_methods[name] == method }
190
195
 
191
196
  # Alias new methods so that the only execute when the object is in this state
192
197
  new_methods.each do |(method_name, _method)|
193
198
  context_name = context_name_for(method_name)
194
- context.class_eval <<-end_eval, __FILE__, __LINE__ + 1
199
+ context.class_eval <<-END_EVAL, __FILE__, __LINE__ + 1
195
200
  alias_method :"#{context_name}", :#{method_name}
196
201
  def #{method_name}(*args, &block)
197
202
  state = self.class.state_machine(#{machine.name.inspect}).states.fetch(#{name.inspect})
198
203
  options = {:method_missing => lambda {super(*args, &block)}, :method_name => #{method_name.inspect}}
199
204
  state.call(self, :"#{context_name}", *(args + [options]), &block)
200
205
  end
201
- end_eval
206
+ END_EVAL
202
207
  end
203
208
 
204
209
  true
@@ -218,29 +223,28 @@ module StateMachines
218
223
  # will be raised.
219
224
  def call(object, method, *args, &block)
220
225
  options = args.last.is_a?(Hash) ? args.pop : {}
221
- options = {method_name: method}.merge(options)
226
+ options = { method_name: method }.merge(options)
222
227
  state = machine.states.match!(object)
223
228
 
224
229
  if state == self && object.respond_to?(method)
225
230
  object.send(method, *args, &block)
226
- elsif method_missing = options[:method_missing]
231
+ elsif (method_missing = options[:method_missing])
227
232
  # Dispatch to the superclass since the object either isn't in this state
228
233
  # or this state doesn't handle the method
229
234
  begin
230
235
  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
236
+ rescue NoMethodError => e
237
+ raise unless e.name.to_s == options[:method_name].to_s && e.args == args
238
+
239
+ # No valid context for this method
240
+ raise InvalidContext.new(object,
241
+ "State #{state.name.inspect} for #{machine.name.inspect} is not a valid context for calling ##{options[:method_name]}")
238
242
  end
239
243
  end
240
244
  end
241
245
 
242
- def draw(graph, options = {})
243
- fail NotImplementedError
246
+ def draw(graph, options = {}, io = $stdout)
247
+ machine.renderer.draw_state(self, graph, options, io)
244
248
  end
245
249
 
246
250
  # Generates a nicely formatted description of this state's contents.
@@ -254,7 +258,7 @@ module StateMachines
254
258
  "#<#{self.class} #{attributes.map { |attr, value| "#{attr}=#{value.inspect}" } * ' '}>"
255
259
  end
256
260
 
257
- private
261
+ private
258
262
 
259
263
  # Should the value be cached after it's evaluated for the first time?
260
264
  def cache_value?
@@ -264,9 +268,16 @@ module StateMachines
264
268
  # Adds a predicate method to the owner class so long as a name has
265
269
  # actually been configured for the state
266
270
  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)
271
+ predicate_method = "#{qualified_name}?"
272
+
273
+ if machine.send(:owner_class_ancestor_has_method?, :instance, predicate_method)
274
+ 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."
275
+ elsif machine.send(:owner_class_has_method?, :instance, predicate_method)
276
+ 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."
277
+ else
278
+ machine.define_helper(:instance, predicate_method) do |machine, object|
279
+ machine.states.matches?(object, name)
280
+ end
270
281
  end
271
282
  end
272
283
 
@@ -1,3 +1,5 @@
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # Represents a module which will get evaluated within the context of a state.
3
5
  #
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ module STDIORenderer
5
+ module_function def draw_machine(machine, io: $stdout)
6
+ draw_class(machine: machine, io: io)
7
+ draw_states(machine: machine, io: io)
8
+ draw_events(machine: machine, io: io)
9
+ end
10
+
11
+ module_function def draw_class(machine:, io: $stdout)
12
+ io.puts "Class: #{machine.owner_class.name}"
13
+ end
14
+
15
+ module_function def draw_states(machine:, io: $stdout)
16
+ io.puts " States:"
17
+ if machine.states.to_a.empty?
18
+ io.puts " - None"
19
+ else
20
+ machine.states.each do |state|
21
+ io.puts " - #{state.name}"
22
+ end
23
+ end
24
+ end
25
+
26
+ module_function def draw_event(event, graph, options: {}, io: $stdout)
27
+ io = io || options[:io] || $stdout
28
+ io.puts " Event: #{event.name}"
29
+ end
30
+
31
+ module_function def draw_branch(branch, graph, event, options: {}, io: $stdout)
32
+ io = io || options[:io] || $stdout
33
+ io.puts " Branch: #{branch.inspect}"
34
+ end
35
+
36
+ module_function def draw_state(state, graph, options: {}, io: $stdout)
37
+ io = io || options[:io] || $stdout
38
+ io.puts " State: #{state.name}"
39
+ end
40
+
41
+ module_function def draw_events(machine:, io: $stdout)
42
+ io.puts " Events:"
43
+ if machine.events.to_a.empty?
44
+ io.puts " - None"
45
+ else
46
+ machine.events.each do |event|
47
+ io.puts " - #{event.name}"
48
+ event.branches.each do |branch|
49
+ branch.state_requirements.each do |requirement|
50
+ out = +" - "
51
+ out << "#{draw_requirement(requirement[:from])} => #{draw_requirement(requirement[:to])}"
52
+ out << " IF #{branch.if_condition}" if branch.if_condition
53
+ out << " UNLESS #{branch.unless_condition}" if branch.unless_condition
54
+ io.puts out
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ module_function def draw_requirement(requirement)
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(', ')
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # A transition represents a state change for a specific attribute.
3
5
  #
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # Represents a collection of transitions in a state machine
3
5
  class TransitionCollection < Array
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
- VERSION = '0.6.0'
4
+ VERSION = '0.10.0'
3
5
  end
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'state_machines/version'
2
4
  require 'state_machines/core'
3
- require 'state_machines/core_ext'
5
+ require 'state_machines/core_ext'
6
+ require 'state_machines/stdio_renderer'
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: state_machines
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
8
8
  - Aaron Pfeifer
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2023-06-30 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: bundler
@@ -56,7 +55,6 @@ dependencies:
56
55
  description: Adds support for creating state machines for attributes on any Ruby class
57
56
  email:
58
57
  - terminale@gmail.com
59
- - aaron@pluginaweek.org
60
58
  executables: []
61
59
  extensions: []
62
60
  extra_rdoc_files: []
@@ -79,6 +77,7 @@ files:
79
77
  - lib/state_machines/integrations.rb
80
78
  - lib/state_machines/integrations/base.rb
81
79
  - lib/state_machines/machine.rb
80
+ - lib/state_machines/machine/class_methods.rb
82
81
  - lib/state_machines/machine_collection.rb
83
82
  - lib/state_machines/macro_methods.rb
84
83
  - lib/state_machines/matcher.rb
@@ -89,14 +88,17 @@ files:
89
88
  - lib/state_machines/state.rb
90
89
  - lib/state_machines/state_collection.rb
91
90
  - lib/state_machines/state_context.rb
91
+ - lib/state_machines/stdio_renderer.rb
92
92
  - lib/state_machines/transition.rb
93
93
  - lib/state_machines/transition_collection.rb
94
94
  - lib/state_machines/version.rb
95
95
  homepage: https://github.com/state-machines/state_machines
96
96
  licenses:
97
97
  - MIT
98
- metadata: {}
99
- post_install_message:
98
+ metadata:
99
+ changelog_uri: https://github.com/state-machines/state_machines/blob/master/CHANGELOG.md
100
+ homepage_uri: https://github.com/state-machines/state_machines
101
+ source_code_uri: https://github.com/state-machines/state_machines
100
102
  rdoc_options: []
101
103
  require_paths:
102
104
  - lib
@@ -111,8 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
113
  - !ruby/object:Gem::Version
112
114
  version: '0'
113
115
  requirements: []
114
- rubygems_version: 3.4.10
115
- signing_key:
116
+ rubygems_version: 3.6.7
116
117
  specification_version: 4
117
118
  summary: State machines for attributes
118
119
  test_files: []