statesman 4.1.3 → 4.1.4

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
- 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