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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.idea/.name +1 -0
  4. data/.idea/.rakeTasks +7 -0
  5. data/.idea/cssxfire.xml +9 -0
  6. data/.idea/encodings.xml +5 -0
  7. data/.idea/misc.xml +5 -0
  8. data/.idea/modules.xml +12 -0
  9. data/.idea/scopes/scope_settings.xml +5 -0
  10. data/.idea/state_machine2.iml +34 -0
  11. data/.idea/vcs.xml +9 -0
  12. data/.idea/workspace.xml +1156 -0
  13. data/.rspec +3 -0
  14. data/.travis.yml +8 -0
  15. data/Gemfile +4 -0
  16. data/LICENSE.txt +23 -0
  17. data/README.md +29 -0
  18. data/Rakefile +1 -0
  19. data/lib/state_machines/assertions.rb +40 -0
  20. data/lib/state_machines/branch.rb +187 -0
  21. data/lib/state_machines/callback.rb +220 -0
  22. data/lib/state_machines/core.rb +25 -0
  23. data/lib/state_machines/core_ext/class/state_machine.rb +5 -0
  24. data/lib/state_machines/core_ext.rb +2 -0
  25. data/lib/state_machines/error.rb +13 -0
  26. data/lib/state_machines/eval_helpers.rb +87 -0
  27. data/lib/state_machines/event.rb +246 -0
  28. data/lib/state_machines/event_collection.rb +141 -0
  29. data/lib/state_machines/extensions.rb +148 -0
  30. data/lib/state_machines/helper_module.rb +17 -0
  31. data/lib/state_machines/integrations/base.rb +100 -0
  32. data/lib/state_machines/integrations.rb +113 -0
  33. data/lib/state_machines/machine.rb +2234 -0
  34. data/lib/state_machines/machine_collection.rb +84 -0
  35. data/lib/state_machines/macro_methods.rb +520 -0
  36. data/lib/state_machines/matcher.rb +123 -0
  37. data/lib/state_machines/matcher_helpers.rb +54 -0
  38. data/lib/state_machines/node_collection.rb +221 -0
  39. data/lib/state_machines/path.rb +120 -0
  40. data/lib/state_machines/path_collection.rb +90 -0
  41. data/lib/state_machines/state.rb +276 -0
  42. data/lib/state_machines/state_collection.rb +112 -0
  43. data/lib/state_machines/state_context.rb +138 -0
  44. data/lib/state_machines/transition.rb +470 -0
  45. data/lib/state_machines/transition_collection.rb +245 -0
  46. data/lib/state_machines/version.rb +3 -0
  47. data/lib/state_machines/yard.rb +8 -0
  48. data/lib/state_machines.rb +3 -0
  49. data/spec/errors/default_spec.rb +14 -0
  50. data/spec/errors/with_message_spec.rb +39 -0
  51. data/spec/helpers/helper_spec.rb +14 -0
  52. data/spec/internal/app/models/auto_shop.rb +31 -0
  53. data/spec/internal/app/models/car.rb +19 -0
  54. data/spec/internal/app/models/model_base.rb +6 -0
  55. data/spec/internal/app/models/motorcycle.rb +9 -0
  56. data/spec/internal/app/models/traffic_light.rb +47 -0
  57. data/spec/internal/app/models/vehicle.rb +123 -0
  58. data/spec/machine_spec.rb +3167 -0
  59. data/spec/matcher_helpers_spec.rb +39 -0
  60. data/spec/matcher_spec.rb +157 -0
  61. data/spec/models/auto_shop_spec.rb +41 -0
  62. data/spec/models/car_spec.rb +90 -0
  63. data/spec/models/motorcycle_spec.rb +44 -0
  64. data/spec/models/traffic_light_spec.rb +56 -0
  65. data/spec/models/vehicle_spec.rb +580 -0
  66. data/spec/node_collection_spec.rb +371 -0
  67. data/spec/path_collection_spec.rb +271 -0
  68. data/spec/path_spec.rb +488 -0
  69. data/spec/spec_helper.rb +6 -0
  70. data/spec/state_collection_spec.rb +352 -0
  71. data/spec/state_context_spec.rb +442 -0
  72. data/spec/state_machine_spec.rb +29 -0
  73. data/spec/state_spec.rb +970 -0
  74. data/spec/support/migration_helpers.rb +50 -0
  75. data/spec/support/models.rb +6 -0
  76. data/spec/transition_collection_spec.rb +2199 -0
  77. data/spec/transition_spec.rb +1558 -0
  78. data/state_machines.gemspec +23 -0
  79. metadata +194 -0
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --warnings
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ script: "bundle exec rake"
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - 2.1.1
7
+ - jruby-19mode
8
+ - rbx-2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in state_machines.gemspec
4
+ gemspec
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,5 @@
1
+ require 'state_machines/macro_methods'
2
+
3
+ Class.class_eval do
4
+ include StateMachines::MacroMethods
5
+ end
@@ -0,0 +1,2 @@
1
+ # Loads all of the extensions to be made to Ruby core classes
2
+ require 'state_machines/core_ext/class/state_machine'
@@ -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