state_machines 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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