statesman 1.3.1 → 2.0.0.rc1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 24961b58213c1edf11c804f50ef70a0eb690d0b8
4
- data.tar.gz: 9817e2fb565978efbeb6f0a90c01f01b8541548a
3
+ metadata.gz: 13d0c38a0446c311aa3db3796688736d190a5643
4
+ data.tar.gz: e13b4b046fe10b8f5dbf07234145b9d75c0b74b0
5
5
  SHA512:
6
- metadata.gz: 0f120a2a604f7e8cdad2927790d59eeb81dbd0459107601fe76f871bbf201dba53f5c5b1c4c15e90472a5a32d9ed3cd9aaed88f93e6d6cb1576bfb4cae594c0e
7
- data.tar.gz: e387129df4c7ac6acb87bf5c7885b16548be3367bcf58f3480d702216c745ac997407548c654eb82eb2ed761cff212711966dd882daafb688638a74737003de9
6
+ metadata.gz: 5ff647a35fe9d98698eae14af6ea89af302d0766da3e6cc7c0a7d920be59fce95782524f19eb58f534f50d8ce4b734c26213642372b02770984454c85f799d43
7
+ data.tar.gz: 082b90d40ea27a482654bfd4d57d48ee089d6f26bd92633b594678bfa75ed9e2024e57ed8213714a2a255d3ec231a29e9326d05e2f94013f4b58a881273403dd
@@ -4,7 +4,6 @@ rvm:
4
4
  - 2.2
5
5
  - 2.1
6
6
  - 2.0.0
7
- - 1.9.3
8
7
 
9
8
  sudo: false
10
9
 
@@ -1,33 +1,59 @@
1
- ## v1.3.1 2 July 2015
1
+ ## v2.0.0.rc1, 23 December 2015
2
+
3
+ *Breaking changes*
4
+
5
+ - Unset most_recent after before transitions
6
+ - TL;DR: set `autosave: false` on the `has_many` association between your parent and transition model and this change will almost certainly not affect your integration
7
+ - Previously the `most_recent` flag would be set to `false` on all transitions during any `before_transition` callbacks
8
+ - After this change, the `most_recent` flag will still be `true` for the previous transition during these callbacks
9
+ - Whilst this behaviour is almost certainly what your integration already expected, as a result of it any attempt to save the new, as yet unpersisted, transition during a `before_transition` callback will result in a uniqueness error. In particular, if you have not set `autosave: false` on the `has_many` association between your parent and transition model then any attempt to save the parent model during a `before_transition` will result in an error
10
+ - Require a most_recent column on transition tables
11
+ - The `most_recent` column, added in v1.2.0, is now required on all transition tables
12
+ - This greatly speeds up queries on large tables
13
+ - A zero-downtime migration path is outlined in the changelog for v1.2.0. You should use that migration path **before** upgrading to v2.0.0
14
+ - Increase default initial sort key to 10
15
+ - Drop support for Ruby 1.9.3, which reached end-of-life in February 2015
16
+ - Move support for events to a companion gem
17
+ - Previously, Statesman supported the use of "events" to trigger transitions
18
+ - To keep Statesman lightweight we've moved event functionality into the `statesman-events` gem
19
+ - If you are using events, add `statesman-events` to your gemfile and include `Statesman::Events` in your state machines
20
+
21
+ *Changes*
22
+
23
+ - Add after_destroy hook to ActiveRecord transition model templates
24
+ - Add `in_state?` instance method to `Statesman::Machine`
25
+ - Add `force_reload` option to `Statesman::Machine#last_transition`
26
+
27
+ ## v1.3.1, 2 July 2015
2
28
 
3
29
  - Fix `in_state` queries with a custom `transition_name` (patch by [0tsuki](https://github.com/0tsuki))
4
- - Fix `backfill_most_recent` rake tast for databases that support partial indexes (patch by [greysteil](https://github.com/greysteil))
30
+ - Fix `backfill_most_recent` rake task for databases that support partial indexes (patch by [greysteil](https://github.com/greysteil))
5
31
 
6
- ## v1.3.0 20 June 2015
32
+ ## v1.3.0, 20 June 2015
7
33
 
8
34
  - Rename `last_transition` alias in `ActiveRecordQueries` to `most_recent_#{model_name}`, to allow merging of two such queries (patch by [@isaacseymour](https://github.com/isaacseymour))
9
35
 
10
- ## v1.2.5 17 June 2015
36
+ ## v1.2.5, 17 June 2015
11
37
 
12
38
  - Make `backfill_most_recent` rake task db-agnostic (patch by [@timothyp](https://github.com/timothyp))
13
39
 
14
- ## v1.2.4 16 June 2015
40
+ ## v1.2.4, 16 June 2015
15
41
 
16
42
  - Clarify error messages when misusing `Statesman::Adapters::ActiveRecordTransition` (patch by [@isaacseymour](https://github.com/isaacseymour))
17
43
 
18
- ## v1.2.3 14 April 2015
44
+ ## v1.2.3, 14 April 2015
19
45
 
20
46
  - Fix use of most_recent column in MySQL (partial indexes aren't supported) (patch by [@greysteil](https://github.com/greysteil))
21
47
 
22
- ## v1.2.2 24 March 2015
48
+ ## v1.2.2, 24 March 2015
23
49
 
24
50
  - Add support for namespaced transition models (patch by [@DanielWright](https://github.com/DanielWright))
25
51
 
26
- ## v1.2.1 24 March 2015
52
+ ## v1.2.1, 24 March 2015
27
53
 
28
54
  - Add support for Postgres 9.4's `jsonb` column type (patch by [@isaacseymour](https://github.com/isaacseymour))
29
55
 
30
- ## v1.2.0 18 March 2015
56
+ ## v1.2.0, 18 March 2015
31
57
 
32
58
  *Changes*
33
59
 
@@ -41,7 +67,7 @@
41
67
  - `ActiveRecordQueries.{not_,}in_state` now accepts an array of states.
42
68
 
43
69
 
44
- ## v1.1.0 9 December 2014
70
+ ## v1.1.0, 9 December 2014
45
71
  *Fixes*
46
72
 
47
73
  - Support for Rails 4.2.0.rc2:
@@ -53,16 +79,16 @@
53
79
 
54
80
  - Transition metadata now defaults to `{}` rather than `nil`. (patch by [@greysteil](https://github.com/greysteil))
55
81
 
56
- ## v1.0.0 21 November 2014
82
+ ## v1.0.0, 21 November 2014
57
83
 
58
84
  No changes from v1.0.0.beta2
59
85
 
60
- ## v1.0.0.beta2 10 October 2014
86
+ ## v1.0.0.beta2, 10 October 2014
61
87
  *Breaking changes*
62
88
 
63
89
  - Rename `ActiveRecordModel` to `ActiveRecordQueries`, to reflect the fact that it mixes in some helpful scopes, but is not required.
64
90
 
65
- ## v1.0.0.beta1 9 October 2014
91
+ ## v1.0.0.beta1, 9 October 2014
66
92
  *Breaking changes*
67
93
 
68
94
  - Classes which include `ActiveRecordModel` must define an `initial_state` class method.
@@ -76,33 +102,33 @@ No changes from v1.0.0.beta2
76
102
  - Transition tables created by generated migrations have `NOT NULL` constraints on `to_state`, `sort_key` and foreign key columns (patch by [@greysteil](https://github.com/greysteil))
77
103
  - `before_transition` and `after_transition` allow an array of to states (patch by [@isaacseymour](https://github.com/isaacseymour))
78
104
 
79
- ## v0.8.3 2 September 2014
105
+ ## v0.8.3, 2 September 2014
80
106
  *Fixes*
81
107
 
82
108
  - Optimisation for Machine#available_events (patch by [@pacso](https://github.com/pacso))
83
109
 
84
- ## v0.8.2 2 September 2014
110
+ ## v0.8.2, 2 September 2014
85
111
  *Fixes*
86
112
 
87
113
  - Stop generating a default value for the metadata column if using MySQL.
88
114
 
89
- ## v0.8.1 19 August 2014
115
+ ## v0.8.1, 19 August 2014
90
116
  *Fixes*
91
117
 
92
118
  - Adds check in Machine#transition to make sure the 'to' state is not an empty array (patch by [@barisbalic](https://github.com/barisbalic))
93
119
 
94
- ## v0.8.0 29 June 2014
120
+ ## v0.8.0, 29 June 2014
95
121
  *Additions*
96
122
 
97
123
  - Events. Machines can now define events as a logical grouping of transitions (patch by [@iurimatias](https://github.com/iurimatias))
98
124
  - Retries. Individual transitions can be executed with a retry policy by wrapping the method call in a `Machine.retry_conflicts {}` block (patch by [@greysteil](https://github.com/greysteil))
99
125
 
100
- ## v0.7.0 25 June 2014
126
+ ## v0.7.0, 25 June 2014
101
127
  *Additions*
102
128
 
103
129
  - `Adapters::ActiveRecord` now handles `ActiveRecord::RecordNotUnique` errors explicitly and re-raises with a `Statesman::TransitionConflictError` if it is due to duplicate sort_keys (patch by [@greysteil](https://github.com/greysteil))
104
130
 
105
- ## v0.6.1 21 May 2014
131
+ ## v0.6.1, 21 May 2014
106
132
  *Fixes*
107
133
  - Fixes an issue where the wrong transition was passed to after_transition callbacks for the second and subsequent transition of a given state machine (patch by [@alan](https://github.com/alan))
108
134
 
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ![Statesman](http://f.cl.ly/items/410n2A0S3l1W0i3i0o2K/statesman.png)
2
2
 
3
- A statesmanlike state machine library for Ruby 1.9.3 and up.
3
+ A statesmanlike state machine library for Ruby 2.0.0 and up.
4
4
 
5
5
  [![Gem Version](https://badge.fury.io/rb/statesman.png)](http://badge.fury.io/rb/statesman)
6
6
  [![Build Status](https://travis-ci.org/gocardless/statesman.png?branch=master)](https://travis-ci.org/gocardless/statesman)
@@ -68,7 +68,7 @@ end
68
68
  class Order < ActiveRecord::Base
69
69
  include Statesman::Adapters::ActiveRecordQueries
70
70
 
71
- has_many :order_transitions
71
+ has_many :order_transitions, autosave: false
72
72
 
73
73
  def state_machine
74
74
  @state_machine ||= OrderStateMachine.new(self, transition_class: OrderTransition)
@@ -134,7 +134,7 @@ And add an association from the parent model:
134
134
 
135
135
  ```ruby
136
136
  class Order < ActiveRecord::Base
137
- has_many :transitions, class_name: "OrderTransition"
137
+ has_many :transitions, class_name: "OrderTransition", autosave: false
138
138
 
139
139
  # Initialize the state machine
140
140
  def state_machine
@@ -255,6 +255,9 @@ number of retry attempts (defaults to 1).
255
255
  #### `Machine#current_state`
256
256
  Returns the current state based on existing transition objects.
257
257
 
258
+ ### `Machine#in_state?(:state_1, :state_2, ...)`
259
+ Returns true if the machine is in any of the given states.
260
+
258
261
  #### `Machine#history`
259
262
  Returns a sorted array of all transition objects.
260
263
 
@@ -304,7 +307,7 @@ need to define a corresponding `transition_name` class method:
304
307
 
305
308
  ```ruby
306
309
  class Order < ActiveRecord::Base
307
- has_many :transitions, class_name: "OrderTransition"
310
+ has_many :transitions, class_name: "OrderTransition", autosave: false
308
311
 
309
312
  private
310
313
 
@@ -352,15 +355,21 @@ Given a field `foo` that was stored in the metadata, you can access it like so:
352
355
  model_instance.last_transition.metadata["foo"]
353
356
  ```
354
357
 
355
- #### Upgrading from 1.1 to 1.2
358
+ #### Events
356
359
 
357
- Statesman 1.2.0 introduced a new `most_recent` column on the transition model,
358
- which is used to speed up queries.
360
+ Used to using a state machine with "events"? Support for events is provided by
361
+ the [statesman-events](https://github.com/gocardless/statesman-events) gem. Once
362
+ that's included in your Gemfile you can include event functionality in your
363
+ state machine as follows:
359
364
 
360
- The change is entirely backwards compatible, but if you'd like the performance
361
- improvements just follow
362
- [this guide](https://github.com/gocardless/statesman/wiki/Adding-a-%60most_recent%60-column-to-an-existing-model)
363
- to add a `most_recent` column to an existing model.
365
+ ```ruby
366
+ class OrderStateMachine
367
+ include Statesman::Machine
368
+ include Statesman::Events
369
+
370
+ ...
371
+ end
372
+ ```
364
373
 
365
374
  ## Testing Statesman Implementations
366
375
 
@@ -5,4 +5,14 @@ class <%= klass %> < ActiveRecord::Base
5
5
  attr_accessible :to_state, :metadata, :sort_key
6
6
  <% end %>
7
7
  belongs_to :<%= parent_name %><%= class_name_option %>, inverse_of: :<%= table_name %>
8
+
9
+ after_destroy :update_most_recent, if: :most_recent?
10
+
11
+ private
12
+
13
+ def update_most_recent
14
+ last_transition = <%= parent_name %>.<%= table_name %>.order(:sort_key).last
15
+ return unless last_transition.present?
16
+ last_transition.update_column(:most_recent, true)
17
+ end
8
18
  end
@@ -51,8 +51,12 @@ module Statesman
51
51
  end
52
52
  end
53
53
 
54
- def last
55
- @last_transition ||= history.last
54
+ def last(force_reload: false)
55
+ if force_reload
56
+ @last_transition = history.last
57
+ else
58
+ @last_transition ||= history.last
59
+ end
56
60
  end
57
61
 
58
62
  private
@@ -62,13 +66,13 @@ module Statesman
62
66
  sort_key: next_sort_key,
63
67
  metadata: metadata }
64
68
 
65
- transition_attributes.merge!(most_recent: true) if most_recent_column?
69
+ transition_attributes.merge!(most_recent: true)
66
70
 
67
71
  transition = transitions_for_parent.build(transition_attributes)
68
72
 
69
73
  ::ActiveRecord::Base.transaction do
70
- unset_old_most_recent
71
74
  @observer.execute(:before, from, to, transition)
75
+ unset_old_most_recent
72
76
  transition.save!
73
77
  @last_transition = transition
74
78
  @observer.execute(:after, from, to, transition)
@@ -83,7 +87,6 @@ module Statesman
83
87
  end
84
88
 
85
89
  def unset_old_most_recent
86
- return unless most_recent_column?
87
90
  # Check whether the `most_recent` column allows null values. If it
88
91
  # doesn't, set old records to `false`, otherwise, set them to `NULL`.
89
92
  #
@@ -98,12 +101,8 @@ module Statesman
98
101
  end
99
102
  end
100
103
 
101
- def most_recent_column?
102
- transition_class.columns_hash.include?("most_recent")
103
- end
104
-
105
104
  def next_sort_key
106
- (last && last.sort_key + 10) || 0
105
+ (last && last.sort_key + 10) || 10
107
106
  end
108
107
 
109
108
  def serialized?(transition_class)
@@ -9,50 +9,19 @@ module Statesman
9
9
  def in_state(*states)
10
10
  states = states.flatten.map(&:to_s)
11
11
 
12
- if use_most_recent_column?
13
- in_state_with_most_recent(states)
14
- else
15
- in_state_without_most_recent(states)
16
- end
12
+ joins(most_recent_transition_join).
13
+ where(states_where(most_recent_transition_alias, states), states)
17
14
  end
18
15
 
19
16
  def not_in_state(*states)
20
17
  states = states.flatten.map(&:to_s)
21
18
 
22
- if use_most_recent_column?
23
- not_in_state_with_most_recent(states)
24
- else
25
- not_in_state_without_most_recent(states)
26
- end
27
- end
28
-
29
- private
30
-
31
- def in_state_with_most_recent(states)
32
- joins(most_recent_transition_join).
33
- where(states_where(most_recent_transition_alias, states), states)
34
- end
35
-
36
- def not_in_state_with_most_recent(states)
37
19
  joins(most_recent_transition_join).
38
20
  where("NOT (#{states_where(most_recent_transition_alias, states)})",
39
21
  states)
40
22
  end
41
23
 
42
- def in_state_without_most_recent(states)
43
- joins(transition1_join).
44
- joins(transition2_join).
45
- where(states_where(most_recent_transition_alias, states), states).
46
- where("#{other_transition_alias}.id" => nil)
47
- end
48
-
49
- def not_in_state_without_most_recent(states)
50
- joins(transition1_join).
51
- joins(transition2_join).
52
- where("NOT (#{states_where(most_recent_transition_alias, states)})",
53
- states).
54
- where("#{other_transition_alias}.id" => nil)
55
- end
24
+ private
56
25
 
57
26
  def transition_class
58
27
  raise NotImplementedError, "A transition_class method should be " \
@@ -80,20 +49,6 @@ module Statesman
80
49
  transition_reflection.table_name
81
50
  end
82
51
 
83
- def transition1_join
84
- "LEFT OUTER JOIN #{model_table} #{most_recent_transition_alias}
85
- ON #{most_recent_transition_alias}.#{model_foreign_key} =
86
- #{table_name}.id"
87
- end
88
-
89
- def transition2_join
90
- "LEFT OUTER JOIN #{model_table} #{other_transition_alias}
91
- ON #{other_transition_alias}.#{model_foreign_key} =
92
- #{table_name}.id
93
- AND #{other_transition_alias}.sort_key >
94
- #{most_recent_transition_alias}.sort_key"
95
- end
96
-
97
52
  def most_recent_transition_join
98
53
  "LEFT OUTER JOIN #{model_table} AS #{most_recent_transition_alias}
99
54
  ON #{table_name}.id =
@@ -115,24 +70,9 @@ module Statesman
115
70
  "most_recent_#{transition_name.to_s.singularize}"
116
71
  end
117
72
 
118
- def other_transition_alias
119
- "other_#{transition_name.to_s.singularize}"
120
- end
121
-
122
73
  def db_true
123
74
  ::ActiveRecord::Base.connection.quote(true)
124
75
  end
125
-
126
- # Only use the most_recent column if it has a unique index guaranteeing
127
- # it has good data
128
- def use_most_recent_column?
129
- ::ActiveRecord::Base.connection.index_exists?(
130
- transition_class.table_name,
131
- [model_foreign_key, :most_recent],
132
- unique: true,
133
- name: "index_#{transition_class.table_name}_parent_most_recent"
134
- )
135
- end
136
76
  end
137
77
  end
138
78
  end
@@ -28,14 +28,14 @@ module Statesman
28
28
  transition
29
29
  end
30
30
 
31
- def last
31
+ def last(*)
32
32
  @history.sort_by(&:sort_key).last
33
33
  end
34
34
 
35
35
  private
36
36
 
37
37
  def next_sort_key
38
- (last && last.sort_key + 10) || 0
38
+ (last && last.sort_key + 10) || 10
39
39
  end
40
40
  end
41
41
  end
@@ -36,8 +36,12 @@ module Statesman
36
36
  transitions_for_parent.asc(:sort_key)
37
37
  end
38
38
 
39
- def last
40
- @last_transition ||= history.last
39
+ def last(force_reload: false)
40
+ if force_reload
41
+ @last_transition = history.last
42
+ else
43
+ @last_transition ||= history.last
44
+ end
41
45
  end
42
46
 
43
47
  private
@@ -55,7 +59,7 @@ module Statesman
55
59
  end
56
60
 
57
61
  def next_sort_key
58
- (last && last.sort_key + 10) || 0
62
+ (last && last.sort_key + 10) || 10
59
63
  end
60
64
  end
61
65
  end
@@ -2,7 +2,6 @@ require_relative "version"
2
2
  require_relative "exceptions"
3
3
  require_relative "guard"
4
4
  require_relative "callback"
5
- require_relative "event_transitions"
6
5
  require_relative "adapters/memory_transition"
7
6
 
8
7
  module Statesman
@@ -32,10 +31,6 @@ module Statesman
32
31
  @states ||= []
33
32
  end
34
33
 
35
- def events
36
- @events ||= {}
37
- end
38
-
39
34
  def state(name, options = { initial: false })
40
35
  name = name.to_s
41
36
  if options[:initial]
@@ -45,10 +40,6 @@ module Statesman
45
40
  states << name
46
41
  end
47
42
 
48
- def event(name, &block)
49
- EventTransitions.new(self, name, &block)
50
- end
51
-
52
43
  def successors
53
44
  @successors ||= {}
54
45
  end
@@ -62,9 +53,9 @@ module Statesman
62
53
  }
63
54
  end
64
55
 
65
- def transition(options = { from: nil, to: nil }, event = nil)
66
- from = to_s_or_nil(options[:from])
67
- to = array_to_s_or_nil(options[:to])
56
+ def transition(from: nil, to: nil)
57
+ from = to_s_or_nil(from)
58
+ to = array_to_s_or_nil(to)
68
59
 
69
60
  raise InvalidStateError, "No to states provided." if to.empty?
70
61
 
@@ -73,12 +64,6 @@ module Statesman
73
64
  ([from] + to).each { |state| validate_state(state) }
74
65
 
75
66
  successors[from] += to
76
-
77
- if event
78
- events[event] ||= {}
79
- events[event][from] ||= []
80
- events[event][from] += to
81
- end
82
67
  end
83
68
 
84
69
  def before_transition(options = { from: nil, to: nil }, &block)
@@ -177,9 +162,9 @@ module Statesman
177
162
  end
178
163
 
179
164
  def initialize(object,
180
- options = {
181
- transition_class: Statesman::Adapters::MemoryTransition
182
- })
165
+ options = {
166
+ transition_class: Statesman::Adapters::MemoryTransition
167
+ })
183
168
  @object = object
184
169
  @transition_class = options[:transition_class]
185
170
  @storage_adapter = adapter_class(@transition_class).new(
@@ -187,19 +172,23 @@ module Statesman
187
172
  send(:after_initialize) if respond_to? :after_initialize
188
173
  end
189
174
 
190
- def current_state
191
- last_action = last_transition
175
+ def current_state(force_reload: false)
176
+ last_action = last_transition(force_reload: force_reload)
192
177
  last_action ? last_action.to_state : self.class.initial_state
193
178
  end
194
179
 
180
+ def in_state?(*states)
181
+ states.flatten.any? { |state| current_state == state.to_s }
182
+ end
183
+
195
184
  def allowed_transitions
196
185
  successors_for(current_state).select do |state|
197
186
  can_transition_to?(state)
198
187
  end
199
188
  end
200
189
 
201
- def last_transition
202
- @storage_adapter.last
190
+ def last_transition(force_reload: false)
191
+ @storage_adapter.last(force_reload: force_reload)
203
192
  end
204
193
 
205
194
  def can_transition_to?(new_state, metadata = {})
@@ -228,21 +217,6 @@ module Statesman
228
217
  true
229
218
  end
230
219
 
231
- def trigger!(event_name, metadata = {})
232
- transitions = self.class.events.fetch(event_name) do
233
- raise Statesman::TransitionFailedError,
234
- "Event #{event_name} not found"
235
- end
236
-
237
- new_state = transitions.fetch(current_state) do
238
- raise Statesman::TransitionFailedError,
239
- "State #{current_state} not found for Event #{event_name}"
240
- end
241
-
242
- transition_to!(new_state.first, metadata)
243
- true
244
- end
245
-
246
220
  def execute(phase, initial_state, new_state, transition)
247
221
  callbacks = callbacks_for(phase, from: initial_state, to: new_state)
248
222
  callbacks.each { |cb| cb.call(@object, transition) }
@@ -254,19 +228,6 @@ module Statesman
254
228
  false
255
229
  end
256
230
 
257
- def trigger(event_name, metadata = {})
258
- self.trigger!(event_name, metadata)
259
- rescue TransitionFailedError, GuardFailedError
260
- false
261
- end
262
-
263
- def available_events
264
- state = current_state
265
- self.class.events.select do |_, transitions|
266
- transitions.key?(state)
267
- end.map(&:first)
268
- end
269
-
270
231
  private
271
232
 
272
233
  def adapter_class(transition_class)