statesman 7.0.1 → 7.4.1
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 +4 -4
- data/.circleci/config.yml +5 -0
- data/CHANGELOG.md +27 -0
- data/Gemfile +1 -1
- data/README.md +1 -1
- data/lib/statesman.rb +12 -1
- data/lib/statesman/adapters/active_record.rb +176 -32
- data/lib/statesman/adapters/memory.rb +4 -0
- data/lib/statesman/config.rb +26 -0
- data/lib/statesman/exceptions.rb +10 -5
- data/lib/statesman/machine.rb +4 -0
- data/lib/statesman/version.rb +1 -1
- data/spec/statesman/adapters/active_record_queries_spec.rb +3 -1
- data/spec/statesman/adapters/active_record_spec.rb +24 -1
- data/spec/statesman/exceptions_spec.rb +88 -0
- data/spec/support/active_record.rb +5 -0
- data/statesman.gemspec +13 -3
- metadata +24 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9dc585aeecc837bf65850c562c9f41524959bd07c5f7f889284f534c260911c3
|
4
|
+
data.tar.gz: 807ae19092dcce330131916b825964e5f62ff13f8c4bde2e2140dee9d1f9983d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2688beb079f3b1993bd19b4460834a18a4ca1d5eac9733003c7c5f08c1fd2deb19aeb46103a8f799e89c346ac0704368119ff180ed907928c74587b6a6623ea0
|
7
|
+
data.tar.gz: c58db63a374f2474ccc73f0064d9dc089d8e52967b047ef77333fdae9b57eb1798cf428a0e6ec162788eaec99e5758d1bd26118c43944f5edda90261dbcb6845
|
data/.circleci/config.yml
CHANGED
@@ -56,6 +56,7 @@ jobs:
|
|
56
56
|
environment:
|
57
57
|
- POSTGRES_USER=postgres
|
58
58
|
- POSTGRES_DB=statesman_test
|
59
|
+
- POSTGRES_PASSWORD=statesman
|
59
60
|
steps: *steps
|
60
61
|
|
61
62
|
build-ruby265-rails-602-mysql:
|
@@ -83,6 +84,7 @@ jobs:
|
|
83
84
|
environment:
|
84
85
|
- POSTGRES_USER=postgres
|
85
86
|
- POSTGRES_DB=statesman_test
|
87
|
+
- POSTGRES_PASSWORD=statesman
|
86
88
|
steps: *steps
|
87
89
|
build-ruby265-rails-master-mysql:
|
88
90
|
docker:
|
@@ -110,6 +112,7 @@ jobs:
|
|
110
112
|
environment:
|
111
113
|
- POSTGRES_USER=postgres
|
112
114
|
- POSTGRES_DB=statesman_test
|
115
|
+
- POSTGRES_PASSWORD=statesman
|
113
116
|
steps: *steps
|
114
117
|
|
115
118
|
build-ruby270-rails-602-mysql:
|
@@ -137,6 +140,7 @@ jobs:
|
|
137
140
|
environment:
|
138
141
|
- POSTGRES_USER=postgres
|
139
142
|
- POSTGRES_DB=statesman_test
|
143
|
+
- POSTGRES_PASSWORD=statesman
|
140
144
|
steps: *steps
|
141
145
|
build-ruby270-rails-master-mysql:
|
142
146
|
docker:
|
@@ -164,6 +168,7 @@ jobs:
|
|
164
168
|
environment:
|
165
169
|
- POSTGRES_USER=postgres
|
166
170
|
- POSTGRES_DB=statesman_test
|
171
|
+
- POSTGRES_PASSWORD=statesman
|
167
172
|
steps: *steps
|
168
173
|
|
169
174
|
workflows:
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,30 @@
|
|
1
|
+
## v7.4.0 26th August 2020
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
- [Gem Metadata](https://guides.rubygems.org/specification-reference/#metadata)
|
6
|
+
to make finding changes between releases even easier.
|
7
|
+
|
8
|
+
## v7.3.0, 24th August 2020
|
9
|
+
|
10
|
+
### Changed
|
11
|
+
|
12
|
+
- Use correct Arel for null [#409](https://github.com/gocardless/statesman/pull/#409)
|
13
|
+
|
14
|
+
## v7.2.0, 19th May 2020
|
15
|
+
|
16
|
+
### Changed
|
17
|
+
|
18
|
+
- Set non-empty password for postgres tests [#398](https://github.com/gocardless/statesman/pull/#398)
|
19
|
+
- Handle transitions differently for MySQL [#399](https://github.com/gocardless/statesman/pull/#399)
|
20
|
+
- pg requirement from >= 0.18, <= 1.1 to >= 0.18, <= 1.3 [#400](https://github.com/gocardless/statesman/pull/#400)
|
21
|
+
- Lazily enable mysql gaplock protection [#402](https://github.com/gocardless/statesman/pull/#402)
|
22
|
+
|
23
|
+
## v7.1.0, 10th Feb 2020
|
24
|
+
|
25
|
+
- Fix `to_s` on `TransitionFailedError` & `GuardFailedError`. `.message` and
|
26
|
+
`.to_s` diverged when `from` and `to` accessors where added in v4.1.3
|
27
|
+
|
1
28
|
## v7.0.1, 8th Jan 2020
|
2
29
|
|
3
30
|
- Fix deprecation warning with Ruby 2.7 [#386](https://github.com/gocardless/statesman/pull/386)
|
data/Gemfile
CHANGED
@@ -14,5 +14,5 @@ end
|
|
14
14
|
|
15
15
|
group :development do
|
16
16
|
# test/unit is no longer bundled with Ruby 2.2, but required by Rails
|
17
|
-
gem "test-unit", "~> 3.
|
17
|
+
gem "test-unit", "~> 3.3" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.2.0")
|
18
18
|
end
|
data/README.md
CHANGED
data/lib/statesman.rb
CHANGED
@@ -20,14 +20,25 @@ module Statesman
|
|
20
20
|
# Example:
|
21
21
|
# Statesman.configure do
|
22
22
|
# storage_adapter Statesman::ActiveRecordAdapter
|
23
|
+
# enable_mysql_gaplock_protection
|
23
24
|
# end
|
24
25
|
#
|
25
26
|
def self.configure(&block)
|
26
|
-
config = Config.new(block)
|
27
|
+
@config = Config.new(block)
|
27
28
|
@storage_adapter = config.adapter_class
|
28
29
|
end
|
29
30
|
|
30
31
|
def self.storage_adapter
|
31
32
|
@storage_adapter || Adapters::Memory
|
32
33
|
end
|
34
|
+
|
35
|
+
def self.mysql_gaplock_protection?
|
36
|
+
return @mysql_gaplock_protection unless @mysql_gaplock_protection.nil?
|
37
|
+
|
38
|
+
@mysql_gaplock_protection = config.mysql_gaplock_protection?
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.config
|
42
|
+
@config ||= Config.new
|
43
|
+
end
|
33
44
|
end
|
@@ -5,9 +5,6 @@ require_relative "../exceptions"
|
|
5
5
|
module Statesman
|
6
6
|
module Adapters
|
7
7
|
class ActiveRecord
|
8
|
-
attr_reader :transition_class
|
9
|
-
attr_reader :parent_model
|
10
|
-
|
11
8
|
JSON_COLUMN_TYPES = %w[json jsonb].freeze
|
12
9
|
|
13
10
|
def self.database_supports_partial_indexes?
|
@@ -19,6 +16,10 @@ module Statesman
|
|
19
16
|
end
|
20
17
|
end
|
21
18
|
|
19
|
+
def self.adapter_name
|
20
|
+
::ActiveRecord::Base.connection.adapter_name.downcase
|
21
|
+
end
|
22
|
+
|
22
23
|
def initialize(transition_class, parent_model, observer, options = {})
|
23
24
|
serialized = serialized?(transition_class)
|
24
25
|
column_type = transition_class.columns_hash["metadata"].sql_type
|
@@ -29,12 +30,15 @@ module Statesman
|
|
29
30
|
end
|
30
31
|
|
31
32
|
@transition_class = transition_class
|
33
|
+
@transition_table = transition_class.arel_table
|
32
34
|
@parent_model = parent_model
|
33
35
|
@observer = observer
|
34
36
|
@association_name =
|
35
37
|
options[:association_name] || @transition_class.table_name
|
36
38
|
end
|
37
39
|
|
40
|
+
attr_reader :transition_class, :transition_table, :parent_model
|
41
|
+
|
38
42
|
def create(from, to, metadata = {})
|
39
43
|
create_transition(from.to_s, to.to_s, metadata)
|
40
44
|
rescue ::ActiveRecord::RecordNotUnique => e
|
@@ -63,21 +67,42 @@ module Statesman
|
|
63
67
|
end
|
64
68
|
end
|
65
69
|
|
70
|
+
def reset
|
71
|
+
@last_transition = nil
|
72
|
+
end
|
73
|
+
|
66
74
|
private
|
67
75
|
|
76
|
+
# rubocop:disable Metrics/MethodLength
|
68
77
|
def create_transition(from, to, metadata)
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
transition_attributes[:most_recent] = true
|
74
|
-
|
75
|
-
transition = transitions_for_parent.build(transition_attributes)
|
78
|
+
transition = transitions_for_parent.build(
|
79
|
+
default_transition_attributes(to, metadata),
|
80
|
+
)
|
76
81
|
|
77
82
|
::ActiveRecord::Base.transaction(requires_new: true) do
|
78
83
|
@observer.execute(:before, from, to, transition)
|
79
|
-
|
80
|
-
|
84
|
+
|
85
|
+
if mysql_gaplock_protection?
|
86
|
+
# We save the transition first with most_recent falsy, then mark most_recent
|
87
|
+
# true after to avoid letting MySQL acquire a next-key lock which can cause
|
88
|
+
# deadlocks.
|
89
|
+
#
|
90
|
+
# To avoid an additional query, we manually adjust the most_recent attribute
|
91
|
+
# on our transition assuming that update_most_recents will have set it to true
|
92
|
+
|
93
|
+
transition.save!
|
94
|
+
|
95
|
+
unless update_most_recents(transition.id).positive?
|
96
|
+
raise ActiveRecord::Rollback, "failed to update most_recent"
|
97
|
+
end
|
98
|
+
|
99
|
+
transition.assign_attributes(most_recent: true)
|
100
|
+
else
|
101
|
+
update_most_recents
|
102
|
+
transition.assign_attributes(most_recent: true)
|
103
|
+
transition.save!
|
104
|
+
end
|
105
|
+
|
81
106
|
@last_transition = transition
|
82
107
|
@observer.execute(:after, from, to, transition)
|
83
108
|
add_after_commit_callback(from, to, transition)
|
@@ -85,6 +110,16 @@ module Statesman
|
|
85
110
|
|
86
111
|
transition
|
87
112
|
end
|
113
|
+
# rubocop:enable Metrics/MethodLength
|
114
|
+
|
115
|
+
def default_transition_attributes(to, metadata)
|
116
|
+
{
|
117
|
+
to_state: to,
|
118
|
+
sort_key: next_sort_key,
|
119
|
+
metadata: metadata,
|
120
|
+
most_recent: not_most_recent_value(db_cast: false),
|
121
|
+
}
|
122
|
+
end
|
88
123
|
|
89
124
|
def add_after_commit_callback(from, to, transition)
|
90
125
|
::ActiveRecord::Base.connection.add_transaction_record(
|
@@ -98,20 +133,92 @@ module Statesman
|
|
98
133
|
parent_model.send(@association_name)
|
99
134
|
end
|
100
135
|
|
101
|
-
|
102
|
-
|
136
|
+
# Sets the given transition most_recent = t while unsetting the most_recent of any
|
137
|
+
# previous transitions.
|
138
|
+
def update_most_recents(most_recent_id = nil)
|
139
|
+
update = build_arel_manager(::Arel::UpdateManager)
|
140
|
+
update.table(transition_table)
|
141
|
+
update.where(most_recent_transitions(most_recent_id))
|
142
|
+
update.set(build_most_recents_update_all_values(most_recent_id))
|
103
143
|
|
104
|
-
#
|
105
|
-
#
|
106
|
-
#
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
144
|
+
# MySQL will validate index constraints across the intermediate result of an
|
145
|
+
# update. This means we must order our update to deactivate the previous
|
146
|
+
# most_recent before setting the new row to be true.
|
147
|
+
update.order(transition_table[:most_recent].desc) if mysql_gaplock_protection?
|
148
|
+
|
149
|
+
::ActiveRecord::Base.connection.update(update.to_sql)
|
150
|
+
end
|
151
|
+
|
152
|
+
def most_recent_transitions(most_recent_id = nil)
|
153
|
+
if most_recent_id
|
154
|
+
transitions_of_parent.and(
|
155
|
+
transition_table[:id].eq(most_recent_id).or(
|
156
|
+
transition_table[:most_recent].eq(true),
|
157
|
+
),
|
158
|
+
)
|
113
159
|
else
|
114
|
-
most_recent.
|
160
|
+
transitions_of_parent.and(transition_table[:most_recent].eq(true))
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def transitions_of_parent
|
165
|
+
transition_table[parent_join_foreign_key.to_sym].eq(parent_model.id)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Generates update_all Arel values that will touch the updated timestamp (if valid
|
169
|
+
# for this model) and set most_recent to true only for the transition with a
|
170
|
+
# matching most_recent ID.
|
171
|
+
#
|
172
|
+
# This is quite nasty, but combines two updates (set all most_recent = f, set
|
173
|
+
# current most_recent = t) into one, which helps improve transition performance
|
174
|
+
# especially when database latency is significant.
|
175
|
+
#
|
176
|
+
# The SQL this can help produce looks like:
|
177
|
+
#
|
178
|
+
# update transitions
|
179
|
+
# set most_recent = (case when id = 'PA123' then TRUE else FALSE end)
|
180
|
+
# , updated_at = '...'
|
181
|
+
# ...
|
182
|
+
#
|
183
|
+
def build_most_recents_update_all_values(most_recent_id = nil)
|
184
|
+
[
|
185
|
+
[
|
186
|
+
transition_table[:most_recent],
|
187
|
+
Arel::Nodes::SqlLiteral.new(most_recent_value(most_recent_id)),
|
188
|
+
],
|
189
|
+
].tap do |values|
|
190
|
+
# Only if we support the updated at timestamps should we add this column to the
|
191
|
+
# update
|
192
|
+
updated_column, updated_at = updated_column_and_timestamp
|
193
|
+
|
194
|
+
if updated_column
|
195
|
+
values << [
|
196
|
+
transition_table[updated_column.to_sym],
|
197
|
+
updated_at,
|
198
|
+
]
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def most_recent_value(most_recent_id)
|
204
|
+
if most_recent_id
|
205
|
+
Arel::Nodes::Case.new.
|
206
|
+
when(transition_table[:id].eq(most_recent_id)).then(db_true).
|
207
|
+
else(not_most_recent_value).to_sql
|
208
|
+
else
|
209
|
+
Arel::Nodes::SqlLiteral.new(not_most_recent_value)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Provide a wrapper for constructing an update manager which handles a breaking API
|
214
|
+
# change in Arel as we move into Rails >6.0.
|
215
|
+
#
|
216
|
+
# https://github.com/rails/rails/commit/7508284800f67b4611c767bff9eae7045674b66f
|
217
|
+
def build_arel_manager(manager)
|
218
|
+
if manager.instance_method(:initialize).arity.zero?
|
219
|
+
manager.new
|
220
|
+
else
|
221
|
+
manager.new(::ActiveRecord::Base)
|
115
222
|
end
|
116
223
|
end
|
117
224
|
|
@@ -170,7 +277,8 @@ module Statesman
|
|
170
277
|
end
|
171
278
|
end
|
172
279
|
|
173
|
-
|
280
|
+
# updated_column_and_timestamp should return [column_name, value]
|
281
|
+
def updated_column_and_timestamp
|
174
282
|
# TODO: Once we've set expectations that transition classes should conform to
|
175
283
|
# the interface of Adapters::ActiveRecordTransition as a breaking change in the
|
176
284
|
# next major version, we can stop calling `#respond_to?` first and instead
|
@@ -184,15 +292,51 @@ module Statesman
|
|
184
292
|
ActiveRecordTransition::DEFAULT_UPDATED_TIMESTAMP_COLUMN
|
185
293
|
end
|
186
294
|
|
187
|
-
|
295
|
+
# No updated timestamp column, don't return anything
|
296
|
+
return nil if column.nil?
|
188
297
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
298
|
+
[
|
299
|
+
column, ::ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
|
300
|
+
]
|
301
|
+
end
|
302
|
+
|
303
|
+
def mysql_gaplock_protection?
|
304
|
+
Statesman.mysql_gaplock_protection?
|
305
|
+
end
|
306
|
+
|
307
|
+
def db_true
|
308
|
+
value = ::ActiveRecord::Base.connection.type_cast(
|
309
|
+
true,
|
310
|
+
transition_class.columns_hash["most_recent"],
|
311
|
+
)
|
312
|
+
::ActiveRecord::Base.connection.quote(value)
|
313
|
+
end
|
314
|
+
|
315
|
+
def db_false
|
316
|
+
value = ::ActiveRecord::Base.connection.type_cast(
|
317
|
+
false,
|
318
|
+
transition_class.columns_hash["most_recent"],
|
319
|
+
)
|
320
|
+
::ActiveRecord::Base.connection.quote(value)
|
321
|
+
end
|
322
|
+
|
323
|
+
def db_null
|
324
|
+
Arel::Nodes::SqlLiteral.new("NULL")
|
325
|
+
end
|
326
|
+
|
327
|
+
# Check whether the `most_recent` column allows null values. If it doesn't, set old
|
328
|
+
# records to `false`, otherwise, set them to `NULL`.
|
329
|
+
#
|
330
|
+
# Some conditioning here is required to support databases that don't support partial
|
331
|
+
# indexes. By doing the conditioning on the column, rather than Rails' opinion of
|
332
|
+
# whether the database supports partial indexes, we're robust to DBs later adding
|
333
|
+
# support for partial indexes.
|
334
|
+
def not_most_recent_value(db_cast: true)
|
335
|
+
if transition_class.columns_hash["most_recent"].null == false
|
336
|
+
return db_cast ? db_false : false
|
337
|
+
end
|
194
338
|
|
195
|
-
|
339
|
+
db_cast ? db_null : nil
|
196
340
|
end
|
197
341
|
end
|
198
342
|
|
data/lib/statesman/config.rb
CHANGED
@@ -14,5 +14,31 @@ module Statesman
|
|
14
14
|
def storage_adapter(adapter_class)
|
15
15
|
@adapter_class = adapter_class
|
16
16
|
end
|
17
|
+
|
18
|
+
def mysql_gaplock_protection?
|
19
|
+
return @mysql_gaplock_protection unless @mysql_gaplock_protection.nil?
|
20
|
+
|
21
|
+
# If our adapter class suggests we're using mysql, enable gaplock protection by
|
22
|
+
# default.
|
23
|
+
enable_mysql_gaplock_protection if mysql_adapter?(adapter_class)
|
24
|
+
@mysql_gaplock_protection
|
25
|
+
end
|
26
|
+
|
27
|
+
def enable_mysql_gaplock_protection
|
28
|
+
@mysql_gaplock_protection = true
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def mysql_adapter?(adapter_class)
|
34
|
+
adapter_name = adapter_name(adapter_class)
|
35
|
+
return false unless adapter_name
|
36
|
+
|
37
|
+
adapter_name.start_with?("mysql")
|
38
|
+
end
|
39
|
+
|
40
|
+
def adapter_name(adapter_class)
|
41
|
+
adapter_class.respond_to?(:adapter_name) && adapter_class&.adapter_name
|
42
|
+
end
|
17
43
|
end
|
18
44
|
end
|
data/lib/statesman/exceptions.rb
CHANGED
@@ -4,16 +4,21 @@ module Statesman
|
|
4
4
|
class InvalidStateError < StandardError; end
|
5
5
|
class InvalidTransitionError < StandardError; end
|
6
6
|
class InvalidCallbackError < StandardError; end
|
7
|
+
class TransitionConflictError < StandardError; end
|
8
|
+
class MissingTransitionAssociation < StandardError; end
|
7
9
|
|
8
10
|
class TransitionFailedError < StandardError
|
9
11
|
def initialize(from, to)
|
10
12
|
@from = from
|
11
13
|
@to = to
|
14
|
+
super(_message)
|
12
15
|
end
|
13
16
|
|
14
17
|
attr_reader :from, :to
|
15
18
|
|
16
|
-
|
19
|
+
private
|
20
|
+
|
21
|
+
def _message
|
17
22
|
"Cannot transition from '#{from}' to '#{to}'"
|
18
23
|
end
|
19
24
|
end
|
@@ -22,18 +27,18 @@ module Statesman
|
|
22
27
|
def initialize(from, to)
|
23
28
|
@from = from
|
24
29
|
@to = to
|
30
|
+
super(_message)
|
25
31
|
end
|
26
32
|
|
27
33
|
attr_reader :from, :to
|
28
34
|
|
29
|
-
|
35
|
+
private
|
36
|
+
|
37
|
+
def _message
|
30
38
|
"Guard on transition from: '#{from}' to '#{to}' returned false"
|
31
39
|
end
|
32
40
|
end
|
33
41
|
|
34
|
-
class TransitionConflictError < StandardError; end
|
35
|
-
class MissingTransitionAssociation < StandardError; end
|
36
|
-
|
37
42
|
class UnserializedMetadataError < StandardError
|
38
43
|
def initialize(transition_class_name)
|
39
44
|
super(_message(transition_class_name))
|
data/lib/statesman/machine.rb
CHANGED
data/lib/statesman/version.rb
CHANGED
@@ -20,7 +20,9 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
20
20
|
prepare_other_model_table
|
21
21
|
prepare_other_transitions_table
|
22
22
|
|
23
|
-
Statesman.configure
|
23
|
+
Statesman.configure do
|
24
|
+
storage_adapter(Statesman::Adapters::ActiveRecord)
|
25
|
+
end
|
24
26
|
end
|
25
27
|
|
26
28
|
after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }
|
@@ -9,9 +9,19 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
9
9
|
before do
|
10
10
|
prepare_model_table
|
11
11
|
prepare_transitions_table
|
12
|
+
|
13
|
+
MyActiveRecordModelTransition.serialize(:metadata, JSON)
|
14
|
+
|
15
|
+
Statesman.configure do
|
16
|
+
# Rubocop requires described_class to be used, but this block
|
17
|
+
# is instance_eval'd and described_class won't be defined
|
18
|
+
# rubocop:disable RSpec/DescribedClass
|
19
|
+
storage_adapter(Statesman::Adapters::ActiveRecord)
|
20
|
+
# rubocop:enable RSpec/DescribedClass
|
21
|
+
end
|
12
22
|
end
|
13
23
|
|
14
|
-
|
24
|
+
after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }
|
15
25
|
|
16
26
|
let(:observer) { double(Statesman::Machine, execute: nil) }
|
17
27
|
let(:model) { MyActiveRecordModel.create(current_state: :pending) }
|
@@ -345,6 +355,19 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
345
355
|
end
|
346
356
|
end
|
347
357
|
|
358
|
+
it "resets last with #reload" do
|
359
|
+
model.save!
|
360
|
+
ActiveRecord::Base.transaction do
|
361
|
+
model.state_machine.transition_to!(:succeeded)
|
362
|
+
# force to cache value in last_transition instance variable
|
363
|
+
expect(model.state_machine.current_state).to eq("succeeded")
|
364
|
+
raise ActiveRecord::Rollback
|
365
|
+
end
|
366
|
+
expect(model.state_machine.current_state).to eq("succeeded")
|
367
|
+
model.reload
|
368
|
+
expect(model.state_machine.current_state).to eq("initial")
|
369
|
+
end
|
370
|
+
|
348
371
|
context "with a namespaced model" do
|
349
372
|
before do
|
350
373
|
CreateNamespacedARModelMigration.migrate(:up)
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe Statesman do
|
6
|
+
describe "InvalidStateError" do
|
7
|
+
subject(:error) { Statesman::InvalidStateError.new }
|
8
|
+
|
9
|
+
its(:message) { is_expected.to eq("Statesman::InvalidStateError") }
|
10
|
+
its "string matches its message" do
|
11
|
+
expect(error.to_s).to eq(error.message)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "InvalidTransitionError" do
|
16
|
+
subject(:error) { Statesman::InvalidTransitionError.new }
|
17
|
+
|
18
|
+
its(:message) { is_expected.to eq("Statesman::InvalidTransitionError") }
|
19
|
+
its "string matches its message" do
|
20
|
+
expect(error.to_s).to eq(error.message)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "InvalidCallbackError" do
|
25
|
+
subject(:error) { Statesman::InvalidTransitionError.new }
|
26
|
+
|
27
|
+
its(:message) { is_expected.to eq("Statesman::InvalidTransitionError") }
|
28
|
+
its "string matches its message" do
|
29
|
+
expect(error.to_s).to eq(error.message)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "TransitionConflictError" do
|
34
|
+
subject(:error) { Statesman::TransitionConflictError.new }
|
35
|
+
|
36
|
+
its(:message) { is_expected.to eq("Statesman::TransitionConflictError") }
|
37
|
+
its "string matches its message" do
|
38
|
+
expect(error.to_s).to eq(error.message)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "MissingTransitionAssociation" do
|
43
|
+
subject(:error) { Statesman::MissingTransitionAssociation.new }
|
44
|
+
|
45
|
+
its(:message) { is_expected.to eq("Statesman::MissingTransitionAssociation") }
|
46
|
+
its "string matches its message" do
|
47
|
+
expect(error.to_s).to eq(error.message)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "TransitionFailedError" do
|
52
|
+
subject(:error) { Statesman::TransitionFailedError.new("from", "to") }
|
53
|
+
|
54
|
+
its(:message) { is_expected.to eq("Cannot transition from 'from' to 'to'") }
|
55
|
+
its "string matches its message" do
|
56
|
+
expect(error.to_s).to eq(error.message)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "GuardFailedError" do
|
61
|
+
subject(:error) { Statesman::GuardFailedError.new("from", "to") }
|
62
|
+
|
63
|
+
its(:message) do
|
64
|
+
is_expected.to eq("Guard on transition from: 'from' to 'to' returned false")
|
65
|
+
end
|
66
|
+
its "string matches its message" do
|
67
|
+
expect(error.to_s).to eq(error.message)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "UnserializedMetadataError" do
|
72
|
+
subject(:error) { Statesman::UnserializedMetadataError.new("foo") }
|
73
|
+
|
74
|
+
its(:message) { is_expected.to match(/foo#metadata is not serialized/) }
|
75
|
+
its "string matches its message" do
|
76
|
+
expect(error.to_s).to eq(error.message)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "IncompatibleSerializationError" do
|
81
|
+
subject(:error) { Statesman::IncompatibleSerializationError.new("foo") }
|
82
|
+
|
83
|
+
its(:message) { is_expected.to match(/foo#metadata column type cannot be json/) }
|
84
|
+
its "string matches its message" do
|
85
|
+
expect(error.to_s).to eq(error.message)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/statesman.gemspec
CHANGED
@@ -4,6 +4,8 @@ lib = File.expand_path("lib", __dir__)
|
|
4
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
5
|
require "statesman/version"
|
6
6
|
|
7
|
+
GITHUB_URL = "https://github.com/gocardless/statesman"
|
8
|
+
|
7
9
|
Gem::Specification.new do |spec|
|
8
10
|
spec.name = "statesman"
|
9
11
|
spec.version = Statesman::VERSION
|
@@ -11,7 +13,7 @@ Gem::Specification.new do |spec|
|
|
11
13
|
spec.email = ["developers@gocardless.com"]
|
12
14
|
spec.description = "A statesman-like state machine library"
|
13
15
|
spec.summary = spec.description
|
14
|
-
spec.homepage =
|
16
|
+
spec.homepage = GITHUB_URL
|
15
17
|
spec.license = "MIT"
|
16
18
|
|
17
19
|
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
@@ -25,7 +27,7 @@ Gem::Specification.new do |spec|
|
|
25
27
|
spec.add_development_dependency "bundler", "~> 2.1.4"
|
26
28
|
spec.add_development_dependency "gc_ruboconfig", "~> 2.3.9"
|
27
29
|
spec.add_development_dependency "mysql2", ">= 0.4", "< 0.6"
|
28
|
-
spec.add_development_dependency "pg", "
|
30
|
+
spec.add_development_dependency "pg", ">= 0.18", "<= 1.3"
|
29
31
|
spec.add_development_dependency "pry"
|
30
32
|
spec.add_development_dependency "rails", ">= 5.2"
|
31
33
|
spec.add_development_dependency "rake", "~> 13.0.0"
|
@@ -33,6 +35,14 @@ Gem::Specification.new do |spec|
|
|
33
35
|
spec.add_development_dependency "rspec-its", "~> 1.1"
|
34
36
|
spec.add_development_dependency "rspec-rails", "~> 3.1"
|
35
37
|
spec.add_development_dependency "rspec_junit_formatter", "~> 0.4.0"
|
36
|
-
spec.add_development_dependency "sqlite3", "~> 1.
|
38
|
+
spec.add_development_dependency "sqlite3", "~> 1.4.2"
|
37
39
|
spec.add_development_dependency "timecop", "~> 0.9.1"
|
40
|
+
|
41
|
+
spec.metadata = {
|
42
|
+
"bug_tracker_uri" => "#{GITHUB_URL}/issues",
|
43
|
+
"changelog_uri" => "#{GITHUB_URL}/blob/master/CHANGELOG.md",
|
44
|
+
"documentation_uri" => "#{GITHUB_URL}/blob/master/README.md",
|
45
|
+
"homepage_uri" => GITHUB_URL,
|
46
|
+
"source_code_uri" => GITHUB_URL,
|
47
|
+
}
|
38
48
|
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: 7.
|
4
|
+
version: 7.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GoCardless
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-11-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ammeter
|
@@ -76,16 +76,22 @@ dependencies:
|
|
76
76
|
name: pg
|
77
77
|
requirement: !ruby/object:Gem::Requirement
|
78
78
|
requirements:
|
79
|
-
- - "
|
79
|
+
- - ">="
|
80
80
|
- !ruby/object:Gem::Version
|
81
81
|
version: '0.18'
|
82
|
+
- - "<="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '1.3'
|
82
85
|
type: :development
|
83
86
|
prerelease: false
|
84
87
|
version_requirements: !ruby/object:Gem::Requirement
|
85
88
|
requirements:
|
86
|
-
- - "
|
89
|
+
- - ">="
|
87
90
|
- !ruby/object:Gem::Version
|
88
91
|
version: '0.18'
|
92
|
+
- - "<="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '1.3'
|
89
95
|
- !ruby/object:Gem::Dependency
|
90
96
|
name: pry
|
91
97
|
requirement: !ruby/object:Gem::Requirement
|
@@ -190,14 +196,14 @@ dependencies:
|
|
190
196
|
requirements:
|
191
197
|
- - "~>"
|
192
198
|
- !ruby/object:Gem::Version
|
193
|
-
version: 1.
|
199
|
+
version: 1.4.2
|
194
200
|
type: :development
|
195
201
|
prerelease: false
|
196
202
|
version_requirements: !ruby/object:Gem::Requirement
|
197
203
|
requirements:
|
198
204
|
- - "~>"
|
199
205
|
- !ruby/object:Gem::Version
|
200
|
-
version: 1.
|
206
|
+
version: 1.4.2
|
201
207
|
- !ruby/object:Gem::Dependency
|
202
208
|
name: timecop
|
203
209
|
requirement: !ruby/object:Gem::Requirement
|
@@ -266,6 +272,7 @@ files:
|
|
266
272
|
- spec/statesman/adapters/shared_examples.rb
|
267
273
|
- spec/statesman/callback_spec.rb
|
268
274
|
- spec/statesman/config_spec.rb
|
275
|
+
- spec/statesman/exceptions_spec.rb
|
269
276
|
- spec/statesman/guard_spec.rb
|
270
277
|
- spec/statesman/machine_spec.rb
|
271
278
|
- spec/statesman/utils_spec.rb
|
@@ -275,8 +282,13 @@ files:
|
|
275
282
|
homepage: https://github.com/gocardless/statesman
|
276
283
|
licenses:
|
277
284
|
- MIT
|
278
|
-
metadata:
|
279
|
-
|
285
|
+
metadata:
|
286
|
+
bug_tracker_uri: https://github.com/gocardless/statesman/issues
|
287
|
+
changelog_uri: https://github.com/gocardless/statesman/blob/master/CHANGELOG.md
|
288
|
+
documentation_uri: https://github.com/gocardless/statesman/blob/master/README.md
|
289
|
+
homepage_uri: https://github.com/gocardless/statesman
|
290
|
+
source_code_uri: https://github.com/gocardless/statesman
|
291
|
+
post_install_message:
|
280
292
|
rdoc_options: []
|
281
293
|
require_paths:
|
282
294
|
- lib
|
@@ -291,8 +303,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
291
303
|
- !ruby/object:Gem::Version
|
292
304
|
version: '0'
|
293
305
|
requirements: []
|
294
|
-
rubygems_version: 3.1
|
295
|
-
signing_key:
|
306
|
+
rubygems_version: 3.2.0.rc.1
|
307
|
+
signing_key:
|
296
308
|
specification_version: 4
|
297
309
|
summary: A statesman-like state machine library
|
298
310
|
test_files:
|
@@ -310,6 +322,7 @@ test_files:
|
|
310
322
|
- spec/statesman/adapters/shared_examples.rb
|
311
323
|
- spec/statesman/callback_spec.rb
|
312
324
|
- spec/statesman/config_spec.rb
|
325
|
+
- spec/statesman/exceptions_spec.rb
|
313
326
|
- spec/statesman/guard_spec.rb
|
314
327
|
- spec/statesman/machine_spec.rb
|
315
328
|
- spec/statesman/utils_spec.rb
|