statesman 1.3.1 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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)