state_machines 0.0.1
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 +7 -0
- data/.gitignore +21 -0
- data/.idea/.name +1 -0
- data/.idea/.rakeTasks +7 -0
- data/.idea/cssxfire.xml +9 -0
- data/.idea/encodings.xml +5 -0
- data/.idea/misc.xml +5 -0
- data/.idea/modules.xml +12 -0
- data/.idea/scopes/scope_settings.xml +5 -0
- data/.idea/state_machine2.iml +34 -0
- data/.idea/vcs.xml +9 -0
- data/.idea/workspace.xml +1156 -0
- data/.rspec +3 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +23 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/lib/state_machines/assertions.rb +40 -0
- data/lib/state_machines/branch.rb +187 -0
- data/lib/state_machines/callback.rb +220 -0
- data/lib/state_machines/core.rb +25 -0
- data/lib/state_machines/core_ext/class/state_machine.rb +5 -0
- data/lib/state_machines/core_ext.rb +2 -0
- data/lib/state_machines/error.rb +13 -0
- data/lib/state_machines/eval_helpers.rb +87 -0
- data/lib/state_machines/event.rb +246 -0
- data/lib/state_machines/event_collection.rb +141 -0
- data/lib/state_machines/extensions.rb +148 -0
- data/lib/state_machines/helper_module.rb +17 -0
- data/lib/state_machines/integrations/base.rb +100 -0
- data/lib/state_machines/integrations.rb +113 -0
- data/lib/state_machines/machine.rb +2234 -0
- data/lib/state_machines/machine_collection.rb +84 -0
- data/lib/state_machines/macro_methods.rb +520 -0
- data/lib/state_machines/matcher.rb +123 -0
- data/lib/state_machines/matcher_helpers.rb +54 -0
- data/lib/state_machines/node_collection.rb +221 -0
- data/lib/state_machines/path.rb +120 -0
- data/lib/state_machines/path_collection.rb +90 -0
- data/lib/state_machines/state.rb +276 -0
- data/lib/state_machines/state_collection.rb +112 -0
- data/lib/state_machines/state_context.rb +138 -0
- data/lib/state_machines/transition.rb +470 -0
- data/lib/state_machines/transition_collection.rb +245 -0
- data/lib/state_machines/version.rb +3 -0
- data/lib/state_machines/yard.rb +8 -0
- data/lib/state_machines.rb +3 -0
- data/spec/errors/default_spec.rb +14 -0
- data/spec/errors/with_message_spec.rb +39 -0
- data/spec/helpers/helper_spec.rb +14 -0
- data/spec/internal/app/models/auto_shop.rb +31 -0
- data/spec/internal/app/models/car.rb +19 -0
- data/spec/internal/app/models/model_base.rb +6 -0
- data/spec/internal/app/models/motorcycle.rb +9 -0
- data/spec/internal/app/models/traffic_light.rb +47 -0
- data/spec/internal/app/models/vehicle.rb +123 -0
- data/spec/machine_spec.rb +3167 -0
- data/spec/matcher_helpers_spec.rb +39 -0
- data/spec/matcher_spec.rb +157 -0
- data/spec/models/auto_shop_spec.rb +41 -0
- data/spec/models/car_spec.rb +90 -0
- data/spec/models/motorcycle_spec.rb +44 -0
- data/spec/models/traffic_light_spec.rb +56 -0
- data/spec/models/vehicle_spec.rb +580 -0
- data/spec/node_collection_spec.rb +371 -0
- data/spec/path_collection_spec.rb +271 -0
- data/spec/path_spec.rb +488 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/state_collection_spec.rb +352 -0
- data/spec/state_context_spec.rb +442 -0
- data/spec/state_machine_spec.rb +29 -0
- data/spec/state_spec.rb +970 -0
- data/spec/support/migration_helpers.rb +50 -0
- data/spec/support/models.rb +6 -0
- data/spec/transition_collection_spec.rb +2199 -0
- data/spec/transition_spec.rb +1558 -0
- data/state_machines.gemspec +23 -0
- metadata +194 -0
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Copyright (c) 2006-2012 Aaron Pfeifer
|
2
|
+
Copyright (c) 2014 Abdelkader Boudih
|
3
|
+
|
4
|
+
MIT License
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
a copy of this software and associated documentation files (the
|
8
|
+
"Software"), to deal in the Software without restriction, including
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
21
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
22
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
23
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# State Machines
|
2
|
+
|
3
|
+
State Machines adds support for creating state machines for attributes on any Ruby class.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'state_machines'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install state_machines
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it ( https://github.com/seuros/state_machines/fork )
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class Hash
|
2
|
+
# Provides a set of helper methods for making assertions about the content
|
3
|
+
# of various objects
|
4
|
+
|
5
|
+
unless defined?(ActiveSupport)
|
6
|
+
# Validate all keys in a hash match <tt>*valid_keys</tt>, raising ArgumentError
|
7
|
+
# on a mismatch. Note that keys are NOT treated indifferently, meaning if you
|
8
|
+
# use strings for keys but assert symbols as keys, this will fail.
|
9
|
+
#
|
10
|
+
# { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
|
11
|
+
# { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
|
12
|
+
# { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing
|
13
|
+
# Code from ActiveSupport
|
14
|
+
def assert_valid_keys(*valid_keys)
|
15
|
+
valid_keys.flatten!
|
16
|
+
each_key do |k|
|
17
|
+
unless valid_keys.include?(k)
|
18
|
+
raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Validates that the given hash only includes at *most* one of a set of
|
25
|
+
# exclusive keys. If more than one key is found, an ArgumentError will be
|
26
|
+
# raised.
|
27
|
+
#
|
28
|
+
# == Examples
|
29
|
+
#
|
30
|
+
# options = {:only => :on, :except => :off}
|
31
|
+
# assert_exclusive_keys(options, :only) # => nil
|
32
|
+
# assert_exclusive_keys(options, :except) # => nil
|
33
|
+
# assert_exclusive_keys(options, :only, :except) # => ArgumentError: Conflicting keys: only, except
|
34
|
+
# assert_exclusive_keys(options, :only, :except, :with) # => ArgumentError: Conflicting keys: only, except
|
35
|
+
def assert_exclusive_keys(*exclusive_keys)
|
36
|
+
conflicting_keys = exclusive_keys & keys
|
37
|
+
raise ArgumentError, "Conflicting keys: #{conflicting_keys.join(', ')}" unless conflicting_keys.length <= 1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'state_machines/matcher'
|
2
|
+
require 'state_machines/eval_helpers'
|
3
|
+
require 'state_machines/assertions'
|
4
|
+
|
5
|
+
module StateMachines
|
6
|
+
# Represents a set of requirements that must be met in order for a transition
|
7
|
+
# or callback to occur. Branches verify that the event, from state, and to
|
8
|
+
# state of the transition match, in addition to if/unless conditionals for
|
9
|
+
# an object's state.
|
10
|
+
class Branch
|
11
|
+
|
12
|
+
include EvalHelpers
|
13
|
+
|
14
|
+
# The condition that must be met on an object
|
15
|
+
attr_reader :if_condition
|
16
|
+
|
17
|
+
# The condition that must *not* be met on an object
|
18
|
+
attr_reader :unless_condition
|
19
|
+
|
20
|
+
# The requirement for verifying the event being matched
|
21
|
+
attr_reader :event_requirement
|
22
|
+
|
23
|
+
# One or more requirements for verifying the states being matched. All
|
24
|
+
# requirements contain a mapping of {:from => matcher, :to => matcher}.
|
25
|
+
attr_reader :state_requirements
|
26
|
+
|
27
|
+
# A list of all of the states known to this branch. This will pull states
|
28
|
+
# from the following options (in the same order):
|
29
|
+
# * +from+ / +except_from+
|
30
|
+
# * +to+ / +except_to+
|
31
|
+
attr_reader :known_states
|
32
|
+
|
33
|
+
# Creates a new branch
|
34
|
+
def initialize(options = {}) #:nodoc:
|
35
|
+
# Build conditionals
|
36
|
+
@if_condition = options.delete(:if)
|
37
|
+
@unless_condition = options.delete(:unless)
|
38
|
+
|
39
|
+
# Build event requirement
|
40
|
+
@event_requirement = build_matcher(options, :on, :except_on)
|
41
|
+
|
42
|
+
if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on]).empty?
|
43
|
+
# Explicit from/to requirements specified
|
44
|
+
@state_requirements = [{:from => build_matcher(options, :from, :except_from), :to => build_matcher(options, :to, :except_to)}]
|
45
|
+
else
|
46
|
+
# Separate out the event requirement
|
47
|
+
options.delete(:on)
|
48
|
+
options.delete(:except_on)
|
49
|
+
|
50
|
+
# Implicit from/to requirements specified
|
51
|
+
@state_requirements = options.collect do |from, to|
|
52
|
+
from = WhitelistMatcher.new(from) unless from.is_a?(Matcher)
|
53
|
+
to = WhitelistMatcher.new(to) unless to.is_a?(Matcher)
|
54
|
+
{:from => from, :to => to}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Track known states. The order that requirements are iterated is based
|
59
|
+
# on the priority in which tracked states should be added.
|
60
|
+
@known_states = []
|
61
|
+
@state_requirements.each do |state_requirement|
|
62
|
+
[:from, :to].each {|option| @known_states |= state_requirement[option].values}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Determines whether the given object / query matches the requirements
|
67
|
+
# configured for this branch. In addition to matching the event, from state,
|
68
|
+
# and to state, this will also check whether the configured :if/:unless
|
69
|
+
# conditions pass on the given object.
|
70
|
+
#
|
71
|
+
# == Examples
|
72
|
+
#
|
73
|
+
# branch = StateMachines::Branch.new(:parked => :idling, :on => :ignite)
|
74
|
+
#
|
75
|
+
# # Successful
|
76
|
+
# branch.matches?(object, :on => :ignite) # => true
|
77
|
+
# branch.matches?(object, :from => nil) # => true
|
78
|
+
# branch.matches?(object, :from => :parked) # => true
|
79
|
+
# branch.matches?(object, :to => :idling) # => true
|
80
|
+
# branch.matches?(object, :from => :parked, :to => :idling) # => true
|
81
|
+
# branch.matches?(object, :on => :ignite, :from => :parked, :to => :idling) # => true
|
82
|
+
#
|
83
|
+
# # Unsuccessful
|
84
|
+
# branch.matches?(object, :on => :park) # => false
|
85
|
+
# branch.matches?(object, :from => :idling) # => false
|
86
|
+
# branch.matches?(object, :to => :first_gear) # => false
|
87
|
+
# branch.matches?(object, :from => :parked, :to => :first_gear) # => false
|
88
|
+
# branch.matches?(object, :on => :park, :from => :parked, :to => :idling) # => false
|
89
|
+
def matches?(object, query = {})
|
90
|
+
!match(object, query).nil?
|
91
|
+
end
|
92
|
+
|
93
|
+
# Attempts to match the given object / query against the set of requirements
|
94
|
+
# configured for this branch. In addition to matching the event, from state,
|
95
|
+
# and to state, this will also check whether the configured :if/:unless
|
96
|
+
# conditions pass on the given object.
|
97
|
+
#
|
98
|
+
# If a match is found, then the event/state requirements that the query
|
99
|
+
# passed successfully will be returned. Otherwise, nil is returned if there
|
100
|
+
# was no match.
|
101
|
+
#
|
102
|
+
# Query options:
|
103
|
+
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
104
|
+
# are specified, then this will always match.
|
105
|
+
# * <tt>:to</tt> - One or more states being transitioned to. If none are
|
106
|
+
# specified, then this will always match.
|
107
|
+
# * <tt>:on</tt> - One or more events that fired the transition. If none
|
108
|
+
# are specified, then this will always match.
|
109
|
+
# * <tt>:guard</tt> - Whether to guard matches with the if/unless
|
110
|
+
# conditionals defined for this branch. Default is true.
|
111
|
+
#
|
112
|
+
# == Examples
|
113
|
+
#
|
114
|
+
# branch = StateMachines::Branch.new(:parked => :idling, :on => :ignite)
|
115
|
+
#
|
116
|
+
# branch.match(object, :on => :ignite) # => {:to => ..., :from => ..., :on => ...}
|
117
|
+
# branch.match(object, :on => :park) # => nil
|
118
|
+
def match(object, query = {})
|
119
|
+
query.assert_valid_keys(:from, :to, :on, :guard)
|
120
|
+
|
121
|
+
if (match = match_query(query)) && matches_conditions?(object, query)
|
122
|
+
match
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def draw(graph, event, valid_states)
|
127
|
+
fail NotImplementedError
|
128
|
+
end
|
129
|
+
|
130
|
+
protected
|
131
|
+
# Builds a matcher strategy to use for the given options. If neither a
|
132
|
+
# whitelist nor a blacklist option is specified, then an AllMatcher is
|
133
|
+
# built.
|
134
|
+
def build_matcher(options, whitelist_option, blacklist_option)
|
135
|
+
options.assert_exclusive_keys(whitelist_option, blacklist_option)
|
136
|
+
|
137
|
+
if options.include?(whitelist_option)
|
138
|
+
value = options[whitelist_option]
|
139
|
+
value.is_a?(Matcher) ? value : WhitelistMatcher.new(options[whitelist_option])
|
140
|
+
elsif options.include?(blacklist_option)
|
141
|
+
value = options[blacklist_option]
|
142
|
+
raise ArgumentError, ":#{blacklist_option} option cannot use matchers; use :#{whitelist_option} instead" if value.is_a?(Matcher)
|
143
|
+
BlacklistMatcher.new(value)
|
144
|
+
else
|
145
|
+
AllMatcher.instance
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Verifies that all configured requirements (event and state) match the
|
150
|
+
# given query. If a match is found, then a hash containing the
|
151
|
+
# event/state requirements that passed will be returned; otherwise, nil.
|
152
|
+
def match_query(query)
|
153
|
+
query ||= {}
|
154
|
+
|
155
|
+
if match_event(query) && (state_requirement = match_states(query))
|
156
|
+
state_requirement.merge(:on => event_requirement)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Verifies that the event requirement matches the given query
|
161
|
+
def match_event(query)
|
162
|
+
matches_requirement?(query, :on, event_requirement)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Verifies that the state requirements match the given query. If a
|
166
|
+
# matching requirement is found, then it is returned.
|
167
|
+
def match_states(query)
|
168
|
+
state_requirements.detect do |state_requirement|
|
169
|
+
[:from, :to].all? {|option| matches_requirement?(query, option, state_requirement[option])}
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Verifies that an option in the given query matches the values required
|
174
|
+
# for that option
|
175
|
+
def matches_requirement?(query, option, requirement)
|
176
|
+
!query.include?(option) || requirement.matches?(query[option], query)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Verifies that the conditionals for this branch evaluate to true for the
|
180
|
+
# given object
|
181
|
+
def matches_conditions?(object, query)
|
182
|
+
query[:guard] == false ||
|
183
|
+
Array(if_condition).all? {|condition| evaluate_method(object, condition)} &&
|
184
|
+
!Array(unless_condition).any? {|condition| evaluate_method(object, condition)}
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
require 'state_machines/branch'
|
2
|
+
require 'state_machines/eval_helpers'
|
3
|
+
|
4
|
+
module StateMachines
|
5
|
+
# Callbacks represent hooks into objects that allow logic to be triggered
|
6
|
+
# before, after, or around a specific set of transitions.
|
7
|
+
class Callback
|
8
|
+
include EvalHelpers
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Determines whether to automatically bind the callback to the object
|
12
|
+
# being transitioned. This only applies to callbacks that are defined as
|
13
|
+
# lambda blocks (or Procs). Some integrations, such as DataMapper, handle
|
14
|
+
# callbacks by executing them bound to the object involved, while other
|
15
|
+
# integrations, such as ActiveRecord, pass the object as an argument to
|
16
|
+
# the callback. This can be configured on an application-wide basis by
|
17
|
+
# setting this configuration to +true+ or +false+. The default value
|
18
|
+
# is +false+.
|
19
|
+
#
|
20
|
+
# *Note* that the DataMapper and Sequel integrations automatically
|
21
|
+
# configure this value on a per-callback basis, so it does not have to
|
22
|
+
# be enabled application-wide.
|
23
|
+
#
|
24
|
+
# == Examples
|
25
|
+
#
|
26
|
+
# When not bound to the object:
|
27
|
+
#
|
28
|
+
# class Vehicle
|
29
|
+
# state_machine do
|
30
|
+
# before_transition do |vehicle|
|
31
|
+
# vehicle.set_alarm
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# def set_alarm
|
36
|
+
# ...
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# When bound to the object:
|
41
|
+
#
|
42
|
+
# StateMachines::Callback.bind_to_object = true
|
43
|
+
#
|
44
|
+
# class Vehicle
|
45
|
+
# state_machine do
|
46
|
+
# before_transition do
|
47
|
+
# self.set_alarm
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# def set_alarm
|
52
|
+
# ...
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
attr_accessor :bind_to_object
|
56
|
+
|
57
|
+
# The application-wide terminator to use for callbacks when not
|
58
|
+
# explicitly defined. Terminators determine whether to cancel a
|
59
|
+
# callback chain based on the return value of the callback.
|
60
|
+
#
|
61
|
+
# See StateMachines::Callback#terminator for more information.
|
62
|
+
attr_accessor :terminator
|
63
|
+
end
|
64
|
+
|
65
|
+
# The type of callback chain this callback is for. This can be one of the
|
66
|
+
# following:
|
67
|
+
# * +before+
|
68
|
+
# * +after+
|
69
|
+
# * +around+
|
70
|
+
# * +failure+
|
71
|
+
attr_accessor :type
|
72
|
+
|
73
|
+
# An optional block for determining whether to cancel the callback chain
|
74
|
+
# based on the return value of the callback. By default, the callback
|
75
|
+
# chain never cancels based on the return value (i.e. there is no implicit
|
76
|
+
# terminator). Certain integrations, such as ActiveRecord and Sequel,
|
77
|
+
# change this default value.
|
78
|
+
#
|
79
|
+
# == Examples
|
80
|
+
#
|
81
|
+
# Canceling the callback chain without a terminator:
|
82
|
+
#
|
83
|
+
# class Vehicle
|
84
|
+
# state_machine do
|
85
|
+
# before_transition do |vehicle|
|
86
|
+
# throw :halt
|
87
|
+
# end
|
88
|
+
# end
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# Canceling the callback chain with a terminator value of +false+:
|
92
|
+
#
|
93
|
+
# class Vehicle
|
94
|
+
# state_machine do
|
95
|
+
# before_transition do |vehicle|
|
96
|
+
# false
|
97
|
+
# end
|
98
|
+
# end
|
99
|
+
# end
|
100
|
+
attr_reader :terminator
|
101
|
+
|
102
|
+
# The branch that determines whether or not this callback can be invoked
|
103
|
+
# based on the context of the transition. The event, from state, and
|
104
|
+
# to state must all match in order for the branch to pass.
|
105
|
+
#
|
106
|
+
# See StateMachines::Branch for more information.
|
107
|
+
attr_reader :branch
|
108
|
+
|
109
|
+
# Creates a new callback that can get called based on the configured
|
110
|
+
# options.
|
111
|
+
#
|
112
|
+
# In addition to the possible configuration options for branches, the
|
113
|
+
# following options can be configured:
|
114
|
+
# * <tt>:bind_to_object</tt> - Whether to bind the callback to the object involved.
|
115
|
+
# If set to false, the object will be passed as a parameter instead.
|
116
|
+
# Default is integration-specific or set to the application default.
|
117
|
+
# * <tt>:terminator</tt> - A block/proc that determines what callback
|
118
|
+
# results should cause the callback chain to halt (if not using the
|
119
|
+
# default <tt>throw :halt</tt> technique).
|
120
|
+
#
|
121
|
+
# More information about how those options affect the behavior of the
|
122
|
+
# callback can be found in their attribute definitions.
|
123
|
+
def initialize(type, *args, &block)
|
124
|
+
@type = type
|
125
|
+
raise ArgumentError, 'Type must be :before, :after, :around, or :failure' unless [:before, :after, :around, :failure].include?(type)
|
126
|
+
|
127
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
128
|
+
@methods = args
|
129
|
+
@methods.concat(Array(options.delete(:do)))
|
130
|
+
@methods << block if block_given?
|
131
|
+
raise ArgumentError, 'Method(s) for callback must be specified' unless @methods.any?
|
132
|
+
|
133
|
+
options = {:bind_to_object => self.class.bind_to_object, :terminator => self.class.terminator}.merge(options)
|
134
|
+
|
135
|
+
# Proxy lambda blocks so that they're bound to the object
|
136
|
+
bind_to_object = options.delete(:bind_to_object)
|
137
|
+
@methods.map! do |method|
|
138
|
+
bind_to_object && method.is_a?(Proc) ? bound_method(method) : method
|
139
|
+
end
|
140
|
+
|
141
|
+
@terminator = options.delete(:terminator)
|
142
|
+
@branch = Branch.new(options)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Gets a list of the states known to this callback by looking at the
|
146
|
+
# branch's known states
|
147
|
+
def known_states
|
148
|
+
branch.known_states
|
149
|
+
end
|
150
|
+
|
151
|
+
# Runs the callback as long as the transition context matches the branch
|
152
|
+
# requirements configured for this callback. If a block is provided, it
|
153
|
+
# will be called when the last method has run.
|
154
|
+
#
|
155
|
+
# If a terminator has been configured and it matches the result from the
|
156
|
+
# evaluated method, then the callback chain should be halted.
|
157
|
+
def call(object, context = {}, *args, &block)
|
158
|
+
if @branch.matches?(object, context)
|
159
|
+
run_methods(object, context, 0, *args, &block)
|
160
|
+
true
|
161
|
+
else
|
162
|
+
false
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
# Runs all of the methods configured for this callback.
|
168
|
+
#
|
169
|
+
# When running +around+ callbacks, this will evaluate each method and
|
170
|
+
# yield when the last method has yielded. The callback will only halt if
|
171
|
+
# one of the methods does not yield.
|
172
|
+
#
|
173
|
+
# For all other types of callbacks, this will evaluate each method in
|
174
|
+
# order. The callback will only halt if the resulting value from the
|
175
|
+
# method passes the terminator.
|
176
|
+
def run_methods(object, context = {}, index = 0, *args, &block)
|
177
|
+
if type == :around
|
178
|
+
current_method = @methods[index]
|
179
|
+
if current_method
|
180
|
+
yielded = false
|
181
|
+
evaluate_method(object, current_method, *args) do
|
182
|
+
yielded = true
|
183
|
+
run_methods(object, context, index + 1, *args, &block)
|
184
|
+
end
|
185
|
+
|
186
|
+
throw :halt unless yielded
|
187
|
+
else
|
188
|
+
yield if block_given?
|
189
|
+
end
|
190
|
+
else
|
191
|
+
@methods.each do |method|
|
192
|
+
result = evaluate_method(object, method, *args)
|
193
|
+
throw :halt if @terminator && @terminator.call(result)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Generates a method that can be bound to the object being transitioned
|
199
|
+
# when the callback is invoked
|
200
|
+
def bound_method(block)
|
201
|
+
type = self.type
|
202
|
+
arity = block.arity
|
203
|
+
arity += 1 if arity >= 0 # Make sure the object gets passed
|
204
|
+
arity += 1 if arity == 1 && type == :around # Make sure the block gets passed
|
205
|
+
|
206
|
+
method = lambda { |object, *args| object.instance_exec(*args, &block) }
|
207
|
+
|
208
|
+
|
209
|
+
# Proxy arity to the original block
|
210
|
+
(
|
211
|
+
class << method;
|
212
|
+
self;
|
213
|
+
end).class_eval do
|
214
|
+
define_method(:arity) { arity }
|
215
|
+
end
|
216
|
+
|
217
|
+
method
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Load all of the core implementation required to use state_machine. This
|
2
|
+
# includes:
|
3
|
+
# * StateMachines::MacroMethods which adds the state_machine DSL to your class
|
4
|
+
# * A set of initializers for setting state_machine defaults based on the current
|
5
|
+
# running environment (such as within Rails)
|
6
|
+
require 'state_machines/error'
|
7
|
+
require 'state_machines/assertions'
|
8
|
+
|
9
|
+
require 'state_machines/machine_collection'
|
10
|
+
require 'state_machines/extensions'
|
11
|
+
|
12
|
+
require 'state_machines/integrations/base'
|
13
|
+
require 'state_machines/integrations'
|
14
|
+
|
15
|
+
require 'state_machines/helper_module'
|
16
|
+
require 'state_machines/state'
|
17
|
+
require 'state_machines/event'
|
18
|
+
require 'state_machines/callback'
|
19
|
+
require 'state_machines/node_collection'
|
20
|
+
require 'state_machines/state_collection'
|
21
|
+
require 'state_machines/event_collection'
|
22
|
+
require 'state_machines/path_collection'
|
23
|
+
require 'state_machines/matcher_helpers'
|
24
|
+
require 'state_machines/machine'
|
25
|
+
require 'state_machines/macro_methods'
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module StateMachines
|
2
|
+
# An error occurred during a state machine invocation
|
3
|
+
class Error < StandardError
|
4
|
+
# The object that failed
|
5
|
+
attr_reader :object
|
6
|
+
|
7
|
+
def initialize(object, message = nil) #:nodoc:
|
8
|
+
@object = object
|
9
|
+
|
10
|
+
super(message)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module StateMachines
|
2
|
+
# Provides a set of helper methods for evaluating methods within the context
|
3
|
+
# of an object.
|
4
|
+
module EvalHelpers
|
5
|
+
# Evaluates one of several different types of methods within the context
|
6
|
+
# of the given object. Methods can be one of the following types:
|
7
|
+
# * Symbol
|
8
|
+
# * Method / Proc
|
9
|
+
# * String
|
10
|
+
#
|
11
|
+
# == Examples
|
12
|
+
#
|
13
|
+
# Below are examples of the various ways that a method can be evaluated
|
14
|
+
# on an object:
|
15
|
+
#
|
16
|
+
# class Person
|
17
|
+
# def initialize(name)
|
18
|
+
# @name = name
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# def name
|
22
|
+
# @name
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# class PersonCallback
|
27
|
+
# def self.run(person)
|
28
|
+
# person.name
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# person = Person.new('John Smith')
|
33
|
+
#
|
34
|
+
# evaluate_method(person, :name) # => "John Smith"
|
35
|
+
# evaluate_method(person, PersonCallback.method(:run)) # => "John Smith"
|
36
|
+
# evaluate_method(person, Proc.new {|person| person.name}) # => "John Smith"
|
37
|
+
# evaluate_method(person, lambda {|person| person.name}) # => "John Smith"
|
38
|
+
# evaluate_method(person, '@name') # => "John Smith"
|
39
|
+
#
|
40
|
+
# == Additional arguments
|
41
|
+
#
|
42
|
+
# Additional arguments can be passed to the methods being evaluated. If
|
43
|
+
# the method defines additional arguments other than the object context,
|
44
|
+
# then all arguments are required.
|
45
|
+
#
|
46
|
+
# For example,
|
47
|
+
#
|
48
|
+
# person = Person.new('John Smith')
|
49
|
+
#
|
50
|
+
# evaluate_method(person, lambda {|person| person.name}, 21) # => "John Smith"
|
51
|
+
# evaluate_method(person, lambda {|person, age| "#{person.name} is #{age}"}, 21) # => "John Smith is 21"
|
52
|
+
# 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)
|
54
|
+
case method
|
55
|
+
when Symbol
|
56
|
+
klass = (class << object; self; end)
|
57
|
+
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
|
+
args.unshift(object)
|
61
|
+
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
|
66
|
+
if [1, 2].include?(arity)
|
67
|
+
# Force the block to be either the only argument or the 2nd one
|
68
|
+
# after the object (may mean additional arguments get discarded)
|
69
|
+
args = args[0, arity - 1] + [block]
|
70
|
+
else
|
71
|
+
# Tack the block to the end of the args
|
72
|
+
args << block
|
73
|
+
end
|
74
|
+
else
|
75
|
+
# These method types are only called with 0, 1, or n arguments
|
76
|
+
args = args[0, arity] if [0, 1].include?(arity)
|
77
|
+
end
|
78
|
+
|
79
|
+
method.is_a?(Proc) ? method.call(*args) : method.call(*args, &block)
|
80
|
+
when String
|
81
|
+
eval(method, object.instance_eval {binding}, &block)
|
82
|
+
else
|
83
|
+
raise ArgumentError, 'Methods must be a symbol denoting the method to call, a block to be invoked, or a string to be evaluated'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|