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.
- checksums.yaml +4 -4
- data/README.md +28 -12
- data/lib/state_machines/assertions.rb +2 -0
- data/lib/state_machines/branch.rb +4 -2
- data/lib/state_machines/callback.rb +2 -0
- data/lib/state_machines/core.rb +2 -0
- data/lib/state_machines/core_ext/class/state_machine.rb +2 -0
- data/lib/state_machines/core_ext.rb +2 -0
- data/lib/state_machines/error.rb +2 -0
- data/lib/state_machines/eval_helpers.rb +38 -9
- data/lib/state_machines/event.rb +4 -2
- data/lib/state_machines/event_collection.rb +2 -0
- data/lib/state_machines/extensions.rb +2 -0
- data/lib/state_machines/helper_module.rb +2 -0
- data/lib/state_machines/integrations/base.rb +2 -0
- data/lib/state_machines/integrations.rb +2 -0
- data/lib/state_machines/machine/class_methods.rb +79 -0
- data/lib/state_machines/machine.rb +16 -63
- data/lib/state_machines/machine_collection.rb +2 -0
- data/lib/state_machines/macro_methods.rb +2 -0
- data/lib/state_machines/matcher.rb +3 -0
- data/lib/state_machines/matcher_helpers.rb +2 -0
- data/lib/state_machines/node_collection.rb +2 -0
- data/lib/state_machines/path.rb +2 -0
- data/lib/state_machines/path_collection.rb +2 -0
- data/lib/state_machines/state.rb +48 -37
- data/lib/state_machines/state_collection.rb +2 -0
- data/lib/state_machines/state_context.rb +2 -0
- data/lib/state_machines/stdio_renderer.rb +74 -0
- data/lib/state_machines/transition.rb +2 -0
- data/lib/state_machines/transition_collection.rb +2 -0
- data/lib/state_machines/version.rb +3 -1
- data/lib/state_machines.rb +4 -1
- metadata +9 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 39c8073bdf33aff2e34bfc3403db686c7d6e4344f158e144cb881230b10d9597
|
4
|
+
data.tar.gz: 150e14164e42addded79421e90456de2f60b4c451c743ee31a8eb2227d91896b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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)
|
584
|
-
* JRuby
|
585
|
-
* Rubinius
|
602
|
+
* Ruby (MRI) 3.0.0+
|
586
603
|
|
587
604
|
For graphing state machine:
|
588
605
|
|
589
|
-
* [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](
|
594
|
-
|
610
|
+
* [state_machines-yard](https://github.com/state-machines/state_machines-yard)
|
595
611
|
|
596
|
-
|
612
|
+
For RSpec testing, use the custom RSpec matchers:
|
597
613
|
|
598
|
-
*
|
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
|
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
|
-
|
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
|
data/lib/state_machines/core.rb
CHANGED
data/lib/state_machines/error.rb
CHANGED
@@ -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
|
60
|
+
object.send(method, *args, **kwargs, &block)
|
61
|
+
when Proc
|
60
62
|
args.unshift(object)
|
61
63
|
arity = method.arity
|
62
|
-
|
63
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/state_machines/event.rb
CHANGED
@@ -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
|
-
|
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.
|
@@ -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?(
|
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?(
|
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
|
1878
|
-
|
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
|
-
|
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
|
# 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 = {})
|
data/lib/state_machines/path.rb
CHANGED
data/lib/state_machines/state.rb
CHANGED
@@ -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
|
-
|
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 = {})
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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)
|
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
|
-
|
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.
|
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 <<-
|
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
|
-
|
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 =>
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
268
|
-
|
269
|
-
|
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
|
|
@@ -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
|
data/lib/state_machines.rb
CHANGED
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.
|
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:
|
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
|
-
|
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.
|
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: []
|