state_machines 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-gemset +1 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +2 -0
  5. data/Changelog.md +7 -1
  6. data/README.md +11 -9
  7. data/lib/state_machines/extensions.rb +1 -1
  8. data/lib/state_machines/integrations.rb +1 -5
  9. data/lib/state_machines/integrations/base.rb +2 -5
  10. data/lib/state_machines/machine.rb +14 -10
  11. data/lib/state_machines/path_collection.rb +3 -3
  12. data/lib/state_machines/state.rb +21 -16
  13. data/lib/state_machines/state_collection.rb +1 -1
  14. data/lib/state_machines/transition.rb +1 -1
  15. data/lib/state_machines/transition_collection.rb +2 -2
  16. data/lib/state_machines/version.rb +1 -1
  17. data/state_machines.gemspec +1 -2
  18. data/test/files/integrations/vehicle.rb +1 -1
  19. data/test/files/models/driver.rb +13 -0
  20. data/test/files/models/motorcycle.rb +5 -0
  21. data/test/functional/driver_default_nonstandard_test.rb +13 -0
  22. data/test/functional/motorcycle_test.rb +6 -0
  23. data/test/unit/event/event_with_matching_disabled_transitions_test.rb +1 -1
  24. data/test/unit/event/event_with_transition_with_nil_to_state_test.rb +2 -2
  25. data/test/unit/integrations/integration_matcher_test.rb +4 -2
  26. data/test/unit/machine/machine_with_custom_integration_test.rb +2 -2
  27. data/test/unit/transition_collection/transition_collection_empty_with_block_test.rb +1 -1
  28. data/test/unit/transition_collection/transition_collection_with_after_callback_halt_test.rb +10 -14
  29. data/test/unit/transition_collection/transition_collection_with_before_callback_halt_test.rb +14 -10
  30. metadata +9 -426
  31. data/test/unit/branch/branch_with_multiple_on_requirements_test.rb +0 -20
  32. data/test/unit/machine_collection/machine_collection_fire_attributes_with_validations_test.rb +0 -72
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2f3852a59e959d4add7fbde2dc0252eee9ac51a7
4
- data.tar.gz: 41ee2c50b0bbcc40691cfe6ff13fed6ab5d14ce7
3
+ metadata.gz: '029961fc61666a778298a5b6e5b5b31802b8710f'
4
+ data.tar.gz: d9e00ff04cbffbaf522e82b40e76aeb9c9ab3564
5
5
  SHA512:
6
- metadata.gz: fa703fb24e2453a0fe96c386b83ab52a633698a8dd7b5d9de9afd48e3d9ee81c058e963d2a7546fe009266e50d22ecd9adfb9575fdb81ebe55dbe9ad166795ac
7
- data.tar.gz: 5f74b1a302c64c4a0d6046decb74bb7e5e167f11bb3675795c6f53d46a2f235c7a91c31f9cd03b1e63c4246330b958937dd113413a2e8c96e6ce1e29f9759292
6
+ metadata.gz: 52f6b57620f17c661585a1bccf2340d46be80366f2a7de609c41033ff46024cc62fc17814279267b26de65974612f149c0a057b47f2396e45f842c249742c9cd
7
+ data.tar.gz: b8bbe1923532edffd936139272782d6053b0ab8f1b338eaf61f805fbbc2a3b1c284bd523c35a375fc40fecb6f4948cb4aabcfc72c0fda5ded46959557a20b1cc
@@ -0,0 +1 @@
1
+ state_machines
@@ -0,0 +1 @@
1
+ 2.4.1
@@ -6,6 +6,8 @@ rvm:
6
6
  - 2.1
7
7
  - 2.0.0
8
8
  - 2.2
9
+ - 2.3.4
10
+ - 2.4.1
9
11
  - jruby
10
12
  - rbx-2
11
13
  matrix:
@@ -1,4 +1,10 @@
1
- * Fixed inconsistent use of :use_transactions
1
+ ## 0.5.0
2
+
3
+ * Fix states being evaluated with wrong `owner_class` context
4
+
5
+ * Fixed state machine false duplication
6
+
7
+ * Fixed inconsistent use of :use_transactions
2
8
 
3
9
  * Namespaced integrations are not registered by default anymore
4
10
 
data/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  [![Build Status](https://travis-ci.org/state-machines/state_machines.svg?branch=master)](https://travis-ci.org/state-machines/state_machines)
2
- [![Code Climate](https://codeclimate.com/github/state-machines/state_machines.png)](https://codeclimate.com/github/state-machines/state_machines)
2
+ [![Code Climate](https://codeclimate.com/github/state-machines/state_machines.svg)](https://codeclimate.com/github/state-machines/state_machines)
3
3
  # State Machines
4
4
 
5
5
  State Machines adds support for creating state machines for attributes on any Ruby class.
6
6
 
7
+ *Please note that multiple integrations are available for [Active Model](https://github.com/state-machines/state_machines-activemodel), [Active Record](https://github.com/state-machines/state_machines-activerecord), [Mongoid](https://github.com/state-machines/state_machines-mongoid) and more in the [State Machines organisation](https://github.com/state-machines).* If you want to save state in your database, **you need one of these additional integrations**.
8
+
7
9
  ## Installation
8
10
 
9
11
  Add this line to your application's Gemfile:
@@ -40,10 +42,10 @@ class Vehicle
40
42
  attr_accessor :seatbelt_on, :time_used, :auto_shop_busy
41
43
 
42
44
  state_machine :state, initial: :parked do
43
- before_transition parked: :any - :parked, do: :put_on_seatbelt
44
-
45
+ before_transition parked: any - :parked, do: :put_on_seatbelt
46
+
45
47
  after_transition on: :crash, do: :tow
46
- after_transition on: :repair, :do: :fix
48
+ after_transition on: :repair, do: :fix
47
49
  after_transition any => :parked do |vehicle, transition|
48
50
  vehicle.seatbelt_on = false
49
51
  end
@@ -83,7 +85,7 @@ class Vehicle
83
85
  event :repair do
84
86
  # The first transition that matches the state and passes its conditions
85
87
  # will be used
86
- transition stalled: parked, unless: :auto_shop_busy
88
+ transition stalled: :parked, unless: :auto_shop_busy
87
89
  transition stalled: same
88
90
  end
89
91
 
@@ -217,7 +219,7 @@ vehicle.fire_events(:shift_down, :enable_alarm) # => true
217
219
  vehicle.state_name # => :first_gear
218
220
  vehicle.alarm_state_name # => :active
219
221
 
220
- vehicle.fire_events!(:ignite, :enable_alarm) # => StateMachines:InvalidTransition: Cannot run events in parallel: ignite, enable_alarm
222
+ vehicle.fire_events!(:ignite, :enable_alarm) # => StateMachines:InvalidParallelTransition: Cannot run events in parallel: ignite, enable_alarm
221
223
 
222
224
  # Human-friendly names can be accessed for states/events
223
225
  Vehicle.human_state_name(:first_gear) # => "first gear"
@@ -400,8 +402,8 @@ For example, transitions and callbacks can be defined like so:
400
402
  class Vehicle
401
403
  state_machine initial: :parked do
402
404
  before_transition from: :parked, except_to: :parked, do: :put_on_seatbelt
403
- after_transition to: :parked do |transition|
404
- self.seatbelt = 'off' # self is the record
405
+ after_transition to: :parked do |vehicle, transition|
406
+ vehicle.seatbelt = 'off'
405
407
  end
406
408
 
407
409
  event :ignite do
@@ -425,7 +427,7 @@ class Vehicle
425
427
  ...
426
428
 
427
429
  state :parked do
428
- transition to::idling, :on => [:ignite, :shift_up], if: :seatbelt_on?
430
+ transition to: :idling, :on => [:ignite, :shift_up], if: :seatbelt_on?
429
431
 
430
432
  def speed
431
433
  0
@@ -133,7 +133,7 @@ module StateMachines
133
133
  # vehicle = Vehicle.new # => #<Vehicle:0xb7c02850 @state="parked", @alarm_state="active">
134
134
  # vehicle.fire_events(:ignite, :disable_alarm) # => true
135
135
  #
136
- # vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachines::InvalidTranstion: Cannot run events in parallel: ignite, disable_alarm
136
+ # vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachines::InvalidParallelTransition: Cannot run events in parallel: ignite, disable_alarm
137
137
  def fire_events!(*events)
138
138
  run_action = [true, false].include?(events.last) ? events.pop : true
139
139
  fire_events(*(events + [run_action])) || fail(StateMachines::InvalidParallelTransition.new(self, events))
@@ -1,5 +1,3 @@
1
- require 'set'
2
-
3
1
  module StateMachines
4
2
  # Integrations allow state machines to take advantage of features within the
5
3
  # context of a particular library. This is currently most useful with
@@ -54,7 +52,6 @@ module StateMachines
54
52
 
55
53
  alias_method :list, :integrations
56
54
 
57
-
58
55
  # Attempts to find an integration that matches the given class. This will
59
56
  # look through all of the built-in integrations under the StateMachines::Integrations
60
57
  # namespace and find one that successfully matches the class.
@@ -86,7 +83,7 @@ module StateMachines
86
83
  # == Examples
87
84
  #
88
85
  # StateMachines::Integrations.match_ancestors([]) # => nil
89
- # StateMachines::Integrations.match_ancestors(['ActiveRecord::Base']) # => StateMachines::Integrations::ActiveModel
86
+ # StateMachines::Integrations.match_ancestors([ActiveRecord::Base]) # => StateMachines::Integrations::ActiveModel
90
87
  def match_ancestors(ancestors)
91
88
  integrations.detect { |integration| integration.matches_ancestors?(ancestors) }
92
89
  end
@@ -103,7 +100,6 @@ module StateMachines
103
100
  integrations.detect { |integration| integration.integration_name == name } || raise(IntegrationNotFound.new(name))
104
101
  end
105
102
 
106
-
107
103
  private
108
104
 
109
105
  def add(integration)
@@ -24,20 +24,17 @@ module StateMachines
24
24
 
25
25
  # Whether the integration should be used for the given class.
26
26
  def matches?(klass)
27
- matches_ancestors?(klass.ancestors.map { |ancestor| ancestor.name })
27
+ matching_ancestors.any? { |ancestor| klass <= ancestor }
28
28
  end
29
29
 
30
30
  # Whether the integration should be used for the given list of ancestors.
31
31
  def matches_ancestors?(ancestors)
32
32
  (ancestors & matching_ancestors).any?
33
33
  end
34
-
35
34
  end
36
35
 
37
- extend ClassMethods
38
-
39
36
  def self.included(base) #:nodoc:
40
- base.class_eval { extend ClassMethods }
37
+ base.extend ClassMethods
41
38
  end
42
39
  end
43
40
  end
@@ -419,7 +419,11 @@ module StateMachines
419
419
  name = args.first || :state
420
420
 
421
421
  # Find an existing machine
422
- if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[name]
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
423
427
  # Only create a new copy if changes are being made to the machine in
424
428
  # a subclass
425
429
  if machine.owner_class != owner_class && (options.any? || block_given?)
@@ -2046,14 +2050,12 @@ module StateMachines
2046
2050
  # the method and is further along in the ancestor chain than this
2047
2051
  # machine's helper module.
2048
2052
  def owner_class_ancestor_has_method?(scope, method)
2053
+ return false unless owner_class_has_method?(scope, method)
2054
+
2049
2055
  superclasses = owner_class.ancestors[1..-1].select { |ancestor| ancestor.is_a?(Class) }
2050
2056
 
2051
2057
  if scope == :class
2052
- # Use singleton classes
2053
- current = (
2054
- class << owner_class;
2055
- self;
2056
- end)
2058
+ current = owner_class.singleton_class
2057
2059
  superclass = superclasses.first
2058
2060
  else
2059
2061
  current = owner_class
@@ -2068,14 +2070,16 @@ module StateMachines
2068
2070
 
2069
2071
  # Search for for the first ancestor that defined this method
2070
2072
  ancestors.detect do |ancestor|
2071
- ancestor = (
2072
- class << ancestor;
2073
- self;
2074
- end) if scope == :class && ancestor.is_a?(Class)
2073
+ ancestor = ancestor.singleton_class if scope == :class && ancestor.is_a?(Class)
2075
2074
  ancestor.method_defined?(method) || ancestor.private_method_defined?(method)
2076
2075
  end
2077
2076
  end
2078
2077
 
2078
+ def owner_class_has_method?(scope, method)
2079
+ target = scope == :class ? owner_class.singleton_class : owner_class
2080
+ target.method_defined?(method) || target.private_method_defined?(method)
2081
+ end
2082
+
2079
2083
  # Adds helper methods for accessing naming information about states and
2080
2084
  # events on the owner class
2081
2085
  def define_name_helpers
@@ -45,7 +45,7 @@ module StateMachines
45
45
  #
46
46
  # paths.from_states # => [:parked, :idling, :first_gear, ...]
47
47
  def from_states
48
- map {|path| path.from_states}.flatten.uniq
48
+ flat_map(&:from_states).uniq
49
49
  end
50
50
 
51
51
  # Lists all of the states that can be transitioned to through the paths in
@@ -55,7 +55,7 @@ module StateMachines
55
55
  #
56
56
  # paths.to_states # => [:idling, :first_gear, :second_gear, ...]
57
57
  def to_states
58
- map {|path| path.to_states}.flatten.uniq
58
+ flat_map(&:to_states).uniq
59
59
  end
60
60
 
61
61
  # Lists all of the events that can be fired through the paths in this
@@ -65,7 +65,7 @@ module StateMachines
65
65
  #
66
66
  # paths.events # => [:park, :ignite, :shift_up, ...]
67
67
  def events
68
- map {|path| path.events}.flatten.uniq
68
+ flat_map(&:events).uniq
69
69
  end
70
70
 
71
71
  private
@@ -2,7 +2,7 @@ module StateMachines
2
2
  # A state defines a value that an attribute can be in after being transitioned
3
3
  # 0 or more times. States can represent a value of any type in Ruby, though
4
4
  # the most common (and default) type is String.
5
- #
5
+ #
6
6
  # In addition to defining the machine's value, a state can also define a
7
7
  # behavioral context for an object when that object is in the state. See
8
8
  # StateMachines::Machine#state for more information about how state-driven
@@ -10,7 +10,7 @@ module StateMachines
10
10
  class State
11
11
 
12
12
  # The state machine for which this state is defined
13
- attr_accessor :machine
13
+ attr_reader :machine
14
14
 
15
15
  # The unique identifier for the state used in event and callback definitions
16
16
  attr_reader :name
@@ -38,7 +38,7 @@ module StateMachines
38
38
  attr_accessor :matcher
39
39
 
40
40
  # Creates a new state within the context of the given machine.
41
- #
41
+ #
42
42
  # Configuration options:
43
43
  # * <tt>:initial</tt> - Whether this state is the beginning state for the
44
44
  # machine. Default is false.
@@ -86,6 +86,11 @@ module StateMachines
86
86
  @context = StateContext.new(self)
87
87
  end
88
88
 
89
+ def machine=(machine)
90
+ @machine = machine
91
+ @context = StateContext.new(self)
92
+ end
93
+
89
94
  # Determines whether there are any states that can be transitioned to from
90
95
  # this state. If there are none, then this state is considered *final*.
91
96
  # Any objects in a final state will remain so forever given the current
@@ -107,15 +112,15 @@ module StateMachines
107
112
  end
108
113
 
109
114
  # Generates a human-readable description of this state's name / value:
110
- #
115
+ #
111
116
  # For example,
112
- #
117
+ #
113
118
  # State.new(machine, :parked).description # => "parked"
114
119
  # State.new(machine, :parked, :value => :parked).description # => "parked"
115
120
  # State.new(machine, :parked, :value => nil).description # => "parked (nil)"
116
121
  # State.new(machine, :parked, :value => 1).description # => "parked (1)"
117
122
  # State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
118
- #
123
+ #
119
124
  # Configuration options:
120
125
  # * <tt>:human_name</tt> - Whether to use this state's human name in the
121
126
  # description or just the internal name
@@ -129,9 +134,9 @@ module StateMachines
129
134
  # The value that represents this state. This will optionally evaluate the
130
135
  # original block if it's a lambda block. Otherwise, the static value is
131
136
  # returned.
132
- #
137
+ #
133
138
  # For example,
134
- #
139
+ #
135
140
  # State.new(machine, :parked, :value => 1).value # => 1
136
141
  # State.new(machine, :parked, :value => lambda {Time.now}).value # => Tue Jan 01 00:00:00 UTC 2008
137
142
  # State.new(machine, :parked, :value => lambda {Time.now}).value(false) # => <Proc:0xb6ea7ca0@...>
@@ -152,14 +157,14 @@ module StateMachines
152
157
  # Determines whether this state matches the given value. If no matcher is
153
158
  # configured, then this will check whether the values are equivalent.
154
159
  # Otherwise, the matcher will determine the result.
155
- #
160
+ #
156
161
  # For example,
157
- #
162
+ #
158
163
  # # Without a matcher
159
164
  # state = State.new(machine, :parked, :value => 1)
160
165
  # state.matches?(1) # => true
161
166
  # state.matches?(2) # => false
162
- #
167
+ #
163
168
  # # With a matcher
164
169
  # state = State.new(machine, :parked, :value => lambda {Time.now}, :if => lambda {|value| !value.nil?})
165
170
  # state.matches?(nil) # => false
@@ -170,7 +175,7 @@ module StateMachines
170
175
 
171
176
  # Defines a context for the state which will be enabled on instances of
172
177
  # the owner class when the machine is in this state.
173
- #
178
+ #
174
179
  # This can be called multiple times. Each time a new context is created,
175
180
  # a new module will be included in the owner class.
176
181
  def context(&block)
@@ -184,7 +189,7 @@ module StateMachines
184
189
  new_methods = context_methods.to_a.select { |(name, method)| old_methods[name] != method }
185
190
 
186
191
  # Alias new methods so that the only execute when the object is in this state
187
- new_methods.each do |(method_name, method)|
192
+ new_methods.each do |(method_name, _method)|
188
193
  context_name = context_name_for(method_name)
189
194
  context.class_eval <<-end_eval, __FILE__, __LINE__ + 1
190
195
  alias_method :"#{context_name}", :#{method_name}
@@ -208,7 +213,7 @@ module StateMachines
208
213
 
209
214
  # Calls a method defined in this state's context on the given object. All
210
215
  # arguments and any block will be passed into the method defined.
211
- #
216
+ #
212
217
  # If the method has never been defined for this state, then a NoMethodError
213
218
  # will be raised.
214
219
  def call(object, method, *args, &block)
@@ -239,9 +244,9 @@ module StateMachines
239
244
  end
240
245
 
241
246
  # Generates a nicely formatted description of this state's contents.
242
- #
247
+ #
243
248
  # For example,
244
- #
249
+ #
245
250
  # state = StateMachines::State.new(machine, :parked, :value => 1, :initial => true)
246
251
  # state # => #<StateMachines::State name=:parked value=1 initial=true context=[]>
247
252
  def inspect
@@ -93,7 +93,7 @@ module StateMachines
93
93
 
94
94
  machine.events.each { |event| order += event.known_states }
95
95
  order += select { |state| state.context_methods.any? }.map { |state| state.name }
96
- order += keys(:name) - machine.callbacks.values.flatten.map { |callback| callback.known_states }.flatten
96
+ order += keys(:name) - machine.callbacks.values.flatten.flat_map(&:known_states)
97
97
  order += keys(:name)
98
98
 
99
99
  order.uniq!
@@ -290,7 +290,7 @@ module StateMachines
290
290
  def pausable
291
291
  begin
292
292
  halted = !catch(:halt) { yield; true }
293
- rescue Exception => error
293
+ rescue => error
294
294
  raise unless @resume_block
295
295
  end
296
296
 
@@ -168,7 +168,7 @@ module StateMachines
168
168
  def catch_exceptions
169
169
  begin
170
170
  yield
171
- rescue Exception
171
+ rescue
172
172
  rollback
173
173
  raise
174
174
  end
@@ -210,7 +210,7 @@ module StateMachines
210
210
  # Rollback only if exceptions occur during before callbacks
211
211
  begin
212
212
  super
213
- rescue Exception
213
+ rescue
214
214
  rollback unless @before_run
215
215
  @success = nil # mimics ActiveRecord.save behavior on rollback
216
216
  raise
@@ -1,3 +1,3 @@
1
1
  module StateMachines
2
- VERSION = '0.4.0'
2
+ VERSION = '0.5.0'
3
3
  end
@@ -12,9 +12,8 @@ Gem::Specification.new do |spec|
12
12
  spec.homepage = 'https://github.com/state-machines/state_machines'
13
13
  spec.license = 'MIT'
14
14
 
15
- spec.required_ruby_version = '>= 1.9.3'
15
+ spec.required_ruby_version = '>= 2.0.0'
16
16
  spec.files = `git ls-files -z`.split("\x0")
17
- spec.test_files = spec.files.grep(/^test\//)
18
17
  spec.require_paths = ['lib']
19
18
 
20
19
  spec.add_development_dependency 'bundler', '>= 1.7.6'
@@ -2,6 +2,6 @@ module VehicleIntegration
2
2
  include StateMachines::Integrations::Base
3
3
 
4
4
  def self.matching_ancestors
5
- ['Vehicle']
5
+ [Vehicle]
6
6
  end
7
7
  end
@@ -0,0 +1,13 @@
1
+ require_relative 'model_base'
2
+
3
+ class Driver < ModelBase
4
+ state_machine :status, :initial => :parked do
5
+ event :park do
6
+ transition :idling => :parked
7
+ end
8
+
9
+ event :ignite do
10
+ transition :parked => :idling
11
+ end
12
+ end
13
+ end