statesman 4.1.3 → 4.1.4

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
- SHA1:
3
- metadata.gz: ad6e72cd43e5b54c25a618524a51aa2029d50a60
4
- data.tar.gz: dd974b992be7e1a764f1b0b58613fe1dbf2951f9
2
+ SHA256:
3
+ metadata.gz: 7445e509f7b9bafc381522bfbdbedac811430da81375bb767472f92386218140
4
+ data.tar.gz: 176143a22965d91c30957bfa7744089947e4269046fac188b89550d737b7cbb8
5
5
  SHA512:
6
- metadata.gz: ac91aa0a9d34410eda08fab8eded5fba9efc280617dad18eb93c6ed91c8ca46f8a34c3e13f5c7d5ca3797053551e4a514b9bcb1d6433cf7fb9ca2d29bf3da423
7
- data.tar.gz: 6223d5ca89126a28905943f0b9e91bef5d5c1c2090e3effb6e78b3849a2c7c01c24069f1171307ae0fb4d3c2757dede80f6a04239f21c0bba7f50af1e6c80fe3
6
+ metadata.gz: afec82cb24e537055105b34d1468f55b4fc220d7dffe49627910fdc7be7e61fbeb044b2e36f53bb111a9c7aeab23cc77b7828c0ac8b3bba167ac461246becc40
7
+ data.tar.gz: d98cf49966c4bb635b98798b44ecab0e5ad77714c794580eeb13939cbff7ab7dfd308de8041e6b95a0a1b56e5e364813896ade88b50422a87ecb00b7294745c0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## v4.1.4, 11th November 2019
2
+
3
+ - Reverts the breaking changes from [PR#358](https://github.com/gocardless/statesman/pull/358) & `v4.1.3` that where included in the last minor release. If you have changed your code to work with these changes `v5.0.0` will be a copy of `v4.1.3` with a bugfix applied.
4
+
1
5
  ## v4.1.3, 6th November 2019
2
6
 
3
7
  - Add accessible from / to state attributes on the `TransitionFailedError` to avoid parsing strings [@ahjmorton](https://github.com/gocardless/statesman/pull/367)
data/README.md CHANGED
@@ -30,7 +30,7 @@ protection.
30
30
  To get started, just add Statesman to your `Gemfile`, and then run `bundle`:
31
31
 
32
32
  ```ruby
33
- gem 'statesman', '~> 3.4.1'
33
+ gem 'statesman', '~> 4.1.4'
34
34
  ```
35
35
 
36
36
  ## Usage
@@ -76,16 +76,22 @@ Then, link it to your model:
76
76
 
77
77
  ```ruby
78
78
  class Order < ActiveRecord::Base
79
- has_many :order_transitions, autosave: false
79
+ include Statesman::Adapters::ActiveRecordQueries
80
80
 
81
- include Statesman::Adapters::ActiveRecordQueries[
82
- transition_class: OrderTransition,
83
- initial_state: :pending
84
- ]
81
+ has_many :order_transitions, autosave: false
85
82
 
86
83
  def state_machine
87
84
  @state_machine ||= OrderStateMachine.new(self, transition_class: OrderTransition)
88
85
  end
86
+
87
+ def self.transition_class
88
+ OrderTransition
89
+ end
90
+
91
+ def self.initial_state
92
+ :pending
93
+ end
94
+ private_class_method :initial_state
89
95
  end
90
96
  ```
91
97
 
@@ -351,34 +357,43 @@ callback code throws an exception, it will not be caught.)
351
357
 
352
358
  A mixin is provided for the ActiveRecord adapter which adds scopes to easily
353
359
  find all models currently in (or not in) a given state. Include it into your
354
- model and passing in `transition_class` and `initial_state` as options.
355
-
356
- In 4.1.1 and below, these two options had to be defined as methods on the model,
357
- but 4.2.0 and above allow this style of configuration as well. The old method
358
- pollutes the model with extra class methods, and is deprecated, to be removed
359
- in 5.0.0.
360
+ model and define `transition_class` and `initial_state` class methods:
360
361
 
361
362
  ```ruby
362
363
  class Order < ActiveRecord::Base
363
- has_many :order_transitions, autosave: false
364
- include Statesman::Adapters::ActiveRecordQueries[
365
- transition_class: OrderTransition,
366
- initial_state: OrderStateMachine.initial_state
367
- ]
364
+ include Statesman::Adapters::ActiveRecordQueries
365
+
366
+ def self.transition_class
367
+ OrderTransition
368
+ end
369
+ private_class_method :transition_class
370
+
371
+ def self.initial_state
372
+ OrderStateMachine.initial_state
373
+ end
374
+ private_class_method :initial_state
368
375
  end
369
376
  ```
370
377
 
371
378
  If the transition class-name differs from the association name, you will also
372
- need to pass `transition_name` as an option:
379
+ need to define a corresponding `transition_name` class method:
373
380
 
374
381
  ```ruby
375
382
  class Order < ActiveRecord::Base
376
383
  has_many :transitions, class_name: "OrderTransition", autosave: false
377
- include Statesman::Adapters::ActiveRecordQueries[
378
- transition_class: OrderTransition,
379
- initial_state: OrderStateMachine.initial_state,
380
- transition_name: :transitions
381
- ]
384
+
385
+ def self.transition_name
386
+ :transitions
387
+ end
388
+
389
+ def self.transition_class
390
+ OrderTransition
391
+ end
392
+
393
+ def self.initial_state
394
+ OrderStateMachine.initial_state
395
+ end
396
+ private_class_method :initial_state
382
397
  end
383
398
  ```
384
399
 
@@ -1,122 +1,51 @@
1
1
  module Statesman
2
2
  module Adapters
3
3
  module ActiveRecordQueries
4
- def self.check_missing_methods!(base)
5
- missing_methods = %i[transition_class initial_state].
6
- reject { |_method| base.respond_to?(:method) }
7
- return if missing_methods.none?
8
-
9
- raise NotImplementedError,
10
- "#{missing_methods.join(', ')} method(s) should be defined on " \
11
- "the model. Alternatively, use the new form of `extend " \
12
- "Statesman::Adapters::ActiveRecordQueries[" \
13
- "transition_class: MyTransition, " \
14
- "initial_state: :some_state]`"
15
- end
16
-
17
4
  def self.included(base)
18
- check_missing_methods!(base)
19
-
20
- base.include(
21
- ClassMethods.new(
22
- transition_class: base.transition_class,
23
- initial_state: base.initial_state,
24
- most_recent_transition_alias: base.try(:most_recent_transition_alias),
25
- transition_name: base.try(:transition_name),
26
- ),
27
- )
5
+ base.extend(ClassMethods)
28
6
  end
29
7
 
30
- def self.[](**args)
31
- ClassMethods.new(**args)
32
- end
33
-
34
- class ClassMethods < Module
35
- def initialize(**args)
36
- @args = args
37
- end
38
-
39
- def included(base)
40
- ensure_inheritance(base)
41
-
42
- query_builder = QueryBuilder.new(base, **@args)
43
-
44
- base.define_singleton_method(:most_recent_transition_join) do
45
- query_builder.most_recent_transition_join
46
- end
47
-
48
- define_in_state(base, query_builder)
49
- define_not_in_state(base, query_builder)
50
- end
8
+ module ClassMethods
9
+ def in_state(*states)
10
+ states = states.flatten.map(&:to_s)
51
11
 
52
- private
53
-
54
- def ensure_inheritance(base)
55
- klass = self
56
- existing_inherited = base.method(:inherited)
57
- base.define_singleton_method(:inherited) do |subclass|
58
- existing_inherited.call(subclass)
59
- subclass.send(:include, klass)
60
- end
12
+ joins(most_recent_transition_join).
13
+ where(states_where(most_recent_transition_alias, states), states)
61
14
  end
62
15
 
63
- def define_in_state(base, query_builder)
64
- base.define_singleton_method(:in_state) do |*states|
65
- states = states.flatten.map(&:to_s)
16
+ def not_in_state(*states)
17
+ states = states.flatten.map(&:to_s)
66
18
 
67
- joins(most_recent_transition_join).
68
- where(query_builder.states_where(states), states)
69
- end
70
- end
71
-
72
- def define_not_in_state(base, query_builder)
73
- base.define_singleton_method(:not_in_state) do |*states|
74
- states = states.flatten.map(&:to_s)
75
-
76
- joins(most_recent_transition_join).
77
- where("NOT (#{query_builder.states_where(states)})", states)
78
- end
79
- end
80
- end
81
-
82
- class QueryBuilder
83
- def initialize(model, transition_class:, initial_state:,
84
- most_recent_transition_alias: nil,
85
- transition_name: nil)
86
- @model = model
87
- @transition_class = transition_class
88
- @initial_state = initial_state
89
- @most_recent_transition_alias = most_recent_transition_alias
90
- @transition_name = transition_name
91
- end
92
-
93
- def states_where(states)
94
- if initial_state.to_s.in?(states.map(&:to_s))
95
- "#{most_recent_transition_alias}.to_state IN (?) OR " \
96
- "#{most_recent_transition_alias}.to_state IS NULL"
97
- else
98
- "#{most_recent_transition_alias}.to_state IN (?) AND " \
99
- "#{most_recent_transition_alias}.to_state IS NOT NULL"
100
- end
19
+ joins(most_recent_transition_join).
20
+ where("NOT (#{states_where(most_recent_transition_alias, states)})",
21
+ states)
101
22
  end
102
23
 
103
24
  def most_recent_transition_join
104
25
  "LEFT OUTER JOIN #{model_table} AS #{most_recent_transition_alias}
105
- ON #{model.table_name}.id =
26
+ ON #{table_name}.id =
106
27
  #{most_recent_transition_alias}.#{model_foreign_key}
107
28
  AND #{most_recent_transition_alias}.most_recent = #{db_true}"
108
29
  end
109
30
 
110
31
  private
111
32
 
112
- attr_reader :model, :transition_class, :initial_state
33
+ def transition_class
34
+ raise NotImplementedError, "A transition_class method should be " \
35
+ "defined on the model"
36
+ end
37
+
38
+ def initial_state
39
+ raise NotImplementedError, "An initial_state method should be " \
40
+ "defined on the model"
41
+ end
113
42
 
114
43
  def transition_name
115
- @transition_name || transition_class.table_name.to_sym
44
+ transition_class.table_name.to_sym
116
45
  end
117
46
 
118
47
  def transition_reflection
119
- model.reflect_on_all_associations(:has_many).each do |value|
48
+ reflect_on_all_associations(:has_many).each do |value|
120
49
  return value if value.klass == transition_class
121
50
  end
122
51
 
@@ -133,9 +62,18 @@ module Statesman
133
62
  transition_reflection.table_name
134
63
  end
135
64
 
65
+ def states_where(temporary_table_name, states)
66
+ if initial_state.to_s.in?(states.map(&:to_s))
67
+ "#{temporary_table_name}.to_state IN (?) OR " \
68
+ "#{temporary_table_name}.to_state IS NULL"
69
+ else
70
+ "#{temporary_table_name}.to_state IN (?) AND " \
71
+ "#{temporary_table_name}.to_state IS NOT NULL"
72
+ end
73
+ end
74
+
136
75
  def most_recent_transition_alias
137
- @most_recent_transition_alias ||
138
- "most_recent_#{transition_name.to_s.singularize}"
76
+ "most_recent_#{transition_name.to_s.singularize}"
139
77
  end
140
78
 
141
79
  def db_true
@@ -1,3 +1,3 @@
1
1
  module Statesman
2
- VERSION = "4.1.3".freeze
2
+ VERSION = "4.1.4".freeze
3
3
  end
@@ -1,17 +1,6 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
4
- def configure_old(klass, transition_class)
5
- klass.define_singleton_method(:transition_class) { transition_class }
6
- klass.define_singleton_method(:initial_state) { :initial }
7
- klass.send(:include, described_class)
8
- end
9
-
10
- def configure_new(klass, transition_class)
11
- klass.send(:include, described_class[transition_class: transition_class,
12
- initial_state: :initial])
13
- end
14
-
15
4
  before do
16
5
  prepare_model_table
17
6
  prepare_transitions_table
@@ -19,6 +8,32 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
19
8
  prepare_other_transitions_table
20
9
 
21
10
  Statesman.configure { storage_adapter(Statesman::Adapters::ActiveRecord) }
11
+
12
+ MyActiveRecordModel.send(:include, Statesman::Adapters::ActiveRecordQueries)
13
+ MyActiveRecordModel.class_eval do
14
+ def self.transition_class
15
+ MyActiveRecordModelTransition
16
+ end
17
+
18
+ def self.initial_state
19
+ :initial
20
+ end
21
+ end
22
+
23
+ OtherActiveRecordModel.send(:include,
24
+ Statesman::Adapters::ActiveRecordQueries)
25
+ OtherActiveRecordModel.class_eval do
26
+ def self.transition_class
27
+ OtherActiveRecordModelTransition
28
+ end
29
+
30
+ def self.initial_state
31
+ :initial
32
+ end
33
+ end
34
+
35
+ MyActiveRecordModel.send(:has_one, :other_active_record_model)
36
+ OtherActiveRecordModel.send(:belongs_to, :my_active_record_model)
22
37
  end
23
38
 
24
39
  after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }
@@ -44,164 +59,105 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
44
59
  model
45
60
  end
46
61
 
47
- shared_examples "testing methods" do
48
- before do
49
- if config_type == :old
50
- configure_old(MyActiveRecordModel, MyActiveRecordModelTransition)
51
- configure_old(OtherActiveRecordModel, OtherActiveRecordModelTransition)
52
- elsif config_type == :new
53
- configure_new(MyActiveRecordModel, MyActiveRecordModelTransition)
54
- configure_new(OtherActiveRecordModel, OtherActiveRecordModelTransition)
55
- else
56
- raise "Unknown config type #{config_type}"
57
- end
62
+ describe ".in_state" do
63
+ context "given a single state" do
64
+ subject { MyActiveRecordModel.in_state(:succeeded) }
58
65
 
59
- MyActiveRecordModel.send(:has_one, :other_active_record_model)
60
- OtherActiveRecordModel.send(:belongs_to, :my_active_record_model)
66
+ it { is_expected.to include model }
67
+ it { is_expected.to_not include other_model }
61
68
  end
62
69
 
63
- describe ".in_state" do
64
- context "given a single state" do
65
- subject { MyActiveRecordModel.in_state(:succeeded) }
66
-
67
- it { is_expected.to include model }
68
- it { is_expected.to_not include other_model }
69
- end
70
+ context "given multiple states" do
71
+ subject { MyActiveRecordModel.in_state(:succeeded, :failed) }
70
72
 
71
- context "given multiple states" do
72
- subject { MyActiveRecordModel.in_state(:succeeded, :failed) }
73
-
74
- it { is_expected.to include model }
75
- it { is_expected.to include other_model }
76
- end
77
-
78
- context "given the initial state" do
79
- subject { MyActiveRecordModel.in_state(:initial) }
80
-
81
- it { is_expected.to include initial_state_model }
82
- it { is_expected.to include returned_to_initial_model }
83
- end
84
-
85
- context "given an array of states" do
86
- subject { MyActiveRecordModel.in_state(%i[succeeded failed]) }
87
-
88
- it { is_expected.to include model }
89
- it { is_expected.to include other_model }
90
- end
73
+ it { is_expected.to include model }
74
+ it { is_expected.to include other_model }
75
+ end
91
76
 
92
- context "merging two queries" do
93
- subject do
94
- MyActiveRecordModel.in_state(:succeeded).
95
- joins(:other_active_record_model).
96
- merge(OtherActiveRecordModel.in_state(:initial))
97
- end
77
+ context "given the initial state" do
78
+ subject { MyActiveRecordModel.in_state(:initial) }
98
79
 
99
- it { is_expected.to be_empty }
100
- end
80
+ it { is_expected.to include initial_state_model }
81
+ it { is_expected.to include returned_to_initial_model }
101
82
  end
102
83
 
103
- describe ".not_in_state" do
104
- context "given a single state" do
105
- subject { MyActiveRecordModel.not_in_state(:failed) }
106
-
107
- it { is_expected.to include model }
108
- it { is_expected.to_not include other_model }
109
- end
84
+ context "given an array of states" do
85
+ subject { MyActiveRecordModel.in_state(%i[succeeded failed]) }
110
86
 
111
- context "given multiple states" do
112
- subject(:not_in_state) { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
87
+ it { is_expected.to include model }
88
+ it { is_expected.to include other_model }
89
+ end
113
90
 
114
- it do
115
- expect(not_in_state).to match_array([initial_state_model,
116
- returned_to_initial_model])
117
- end
91
+ context "merging two queries" do
92
+ subject do
93
+ MyActiveRecordModel.in_state(:succeeded).
94
+ joins(:other_active_record_model).
95
+ merge(OtherActiveRecordModel.in_state(:initial))
118
96
  end
119
97
 
120
- context "given an array of states" do
121
- subject(:not_in_state) { MyActiveRecordModel.not_in_state(%i[succeeded failed]) }
122
-
123
- it do
124
- expect(not_in_state).to match_array([initial_state_model,
125
- returned_to_initial_model])
126
- end
127
- end
98
+ it { is_expected.to be_empty }
128
99
  end
100
+ end
129
101
 
130
- context "with a custom name for the transition association" do
131
- before do
132
- # Switch to using OtherActiveRecordModelTransition, so the existing
133
- # relation with MyActiveRecordModelTransition doesn't interfere with
134
- # this spec.
135
- MyActiveRecordModel.send(:has_many,
136
- :custom_name,
137
- class_name: "OtherActiveRecordModelTransition")
138
-
139
- MyActiveRecordModel.class_eval do
140
- def self.transition_class
141
- OtherActiveRecordModelTransition
142
- end
143
- end
144
- end
145
-
146
- describe ".in_state" do
147
- subject(:query) { MyActiveRecordModel.in_state(:succeeded) }
102
+ describe ".not_in_state" do
103
+ context "given a single state" do
104
+ subject { MyActiveRecordModel.not_in_state(:failed) }
148
105
 
149
- specify { expect { query }.to_not raise_error }
150
- end
106
+ it { is_expected.to include model }
107
+ it { is_expected.to_not include other_model }
151
108
  end
152
109
 
153
- context "after_commit transactional integrity" do
154
- before do
155
- MyStateMachine.class_eval do
156
- cattr_accessor(:after_commit_callback_executed) { false }
110
+ context "given multiple states" do
111
+ subject(:not_in_state) { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
157
112
 
158
- after_transition(from: :initial, to: :succeeded, after_commit: true) do
159
- # This leaks state in a testable way if transactional integrity is broken.
160
- MyStateMachine.after_commit_callback_executed = true
161
- end
162
- end
163
- end
164
-
165
- after do
166
- MyStateMachine.class_eval do
167
- callbacks[:after_commit] = []
168
- end
113
+ it do
114
+ expect(not_in_state).to match_array([initial_state_model,
115
+ returned_to_initial_model])
169
116
  end
117
+ end
170
118
 
171
- let!(:model) do
172
- MyActiveRecordModel.create
173
- end
119
+ context "given an array of states" do
120
+ subject(:not_in_state) { MyActiveRecordModel.not_in_state(%i[succeeded failed]) }
174
121
 
175
- # rubocop:disable RSpec/ExampleLength
176
122
  it do
177
- expect do
178
- ActiveRecord::Base.transaction do
179
- model.state_machine.transition_to!(:succeeded)
180
- raise ActiveRecord::Rollback
181
- end
182
- end.to_not change(MyStateMachine, :after_commit_callback_executed)
123
+ expect(not_in_state).to match_array([initial_state_model,
124
+ returned_to_initial_model])
183
125
  end
184
- # rubocop:enable RSpec/ExampleLength
185
126
  end
186
127
  end
187
128
 
188
- context "using old configuration method" do
189
- let(:config_type) { :old }
190
-
191
- include_examples "testing methods"
192
- end
129
+ context "with a custom name for the transition association" do
130
+ before do
131
+ # Switch to using OtherActiveRecordModelTransition, so the existing
132
+ # relation with MyActiveRecordModelTransition doesn't interfere with
133
+ # this spec.
134
+ MyActiveRecordModel.send(:has_many,
135
+ :custom_name,
136
+ class_name: "OtherActiveRecordModelTransition")
137
+
138
+ MyActiveRecordModel.class_eval do
139
+ def self.transition_class
140
+ OtherActiveRecordModelTransition
141
+ end
142
+ end
143
+ end
193
144
 
194
- context "using new configuration method" do
195
- let(:config_type) { :new }
145
+ describe ".in_state" do
146
+ subject(:query) { MyActiveRecordModel.in_state(:succeeded) }
196
147
 
197
- include_examples "testing methods"
148
+ specify { expect { query }.to_not raise_error }
149
+ end
198
150
  end
199
151
 
200
152
  context "with no association with the transition class" do
201
153
  before do
202
154
  class UnknownModelTransition < OtherActiveRecordModelTransition; end
203
155
 
204
- configure_old(MyActiveRecordModel, UnknownModelTransition)
156
+ MyActiveRecordModel.class_eval do
157
+ def self.transition_class
158
+ UnknownModelTransition
159
+ end
160
+ end
205
161
  end
206
162
 
207
163
  describe ".in_state" do
@@ -212,4 +168,38 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
212
168
  end
213
169
  end
214
170
  end
171
+
172
+ context "after_commit transactional integrity" do
173
+ before do
174
+ MyStateMachine.class_eval do
175
+ cattr_accessor(:after_commit_callback_executed) { false }
176
+
177
+ after_transition(from: :initial, to: :succeeded, after_commit: true) do
178
+ # This leaks state in a testable way if transactional integrity is broken.
179
+ MyStateMachine.after_commit_callback_executed = true
180
+ end
181
+ end
182
+ end
183
+
184
+ after do
185
+ MyStateMachine.class_eval do
186
+ callbacks[:after_commit] = []
187
+ end
188
+ end
189
+
190
+ let!(:model) do
191
+ MyActiveRecordModel.create
192
+ end
193
+
194
+ # rubocop:disable RSpec/ExampleLength
195
+ it do
196
+ expect do
197
+ ActiveRecord::Base.transaction do
198
+ model.state_machine.transition_to!(:succeeded)
199
+ raise ActiveRecord::Rollback
200
+ end
201
+ end.to_not change(MyStateMachine, :after_commit_callback_executed)
202
+ end
203
+ # rubocop:enable RSpec/ExampleLength
204
+ end
215
205
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statesman
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.3
4
+ version: 4.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - GoCardless
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-06 00:00:00.000000000 Z
11
+ date: 2019-11-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ammeter
@@ -292,7 +292,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
292
292
  version: '0'
293
293
  requirements: []
294
294
  rubyforge_project:
295
- rubygems_version: 2.5.2
295
+ rubygems_version: 2.7.6.2
296
296
  signing_key:
297
297
  specification_version: 4
298
298
  summary: A statesman-like state machine library