statesman 7.0.0 → 7.4.0
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 +71 -8
- data/CHANGELOG.md +31 -0
- data/Gemfile +1 -1
- data/README.md +1 -1
- data/lib/statesman.rb +12 -1
- data/lib/statesman/adapters/active_record.rb +174 -34
- data/lib/statesman/config.rb +26 -0
- data/lib/statesman/exceptions.rb +10 -5
- 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 +11 -1
- data/spec/statesman/exceptions_spec.rb +88 -0
- data/statesman.gemspec +14 -4
- metadata +26 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f0dbcc9fe3c29e715be585f2ddae96bdea9b80ca104daf00ee3378feab681b8e
|
|
4
|
+
data.tar.gz: c5824ece991650b8e6e979a9899dbdabc38b18209a79260f2aa9e9f4fda67da2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 98fe2966e48e912b33f1c6d842aa6090782dd32be726532d8178377ffb0f1a551ee3d83eee291de0723c756eb0b1bda65dae73d695a47f8b6bd38106b127615f
|
|
7
|
+
data.tar.gz: 6be77a7114e2c3d59e76cfa71de583213ead8a171c5039941698c1e4b0b28b68500acba91d498fb488d33d0e31b3274fa5e373ce81a55f9f70e8e229132de1cf
|
data/.circleci/config.yml
CHANGED
|
@@ -12,7 +12,7 @@ references:
|
|
|
12
12
|
- type: cache-restore
|
|
13
13
|
key: statesman-{{ checksum "Gemfile" }}-{{ checksum "~/RAILS_VERSION.txt" }}
|
|
14
14
|
|
|
15
|
-
- run: gem install bundler -v 1.
|
|
15
|
+
- run: gem install bundler -v 2.1.4
|
|
16
16
|
|
|
17
17
|
- run: bundle install --path vendor/bundle
|
|
18
18
|
|
|
@@ -31,6 +31,34 @@ references:
|
|
|
31
31
|
path: /tmp/test-results
|
|
32
32
|
|
|
33
33
|
jobs:
|
|
34
|
+
build-ruby249-rails-524-mysql:
|
|
35
|
+
docker:
|
|
36
|
+
- image: circleci/ruby:2.4.9-node
|
|
37
|
+
environment:
|
|
38
|
+
- RAILS_VERSION=5.2.4
|
|
39
|
+
- DATABASE_URL=mysql2://root@127.0.0.1/statesman_test
|
|
40
|
+
- DATABASE_DEPENDENCY_PORT=3306
|
|
41
|
+
- image: circleci/mysql:5.7.18
|
|
42
|
+
environment:
|
|
43
|
+
- MYSQL_ALLOW_EMPTY_PASSWORD=true
|
|
44
|
+
- MYSQL_USER=root
|
|
45
|
+
- MYSQL_PASSWORD=
|
|
46
|
+
- MYSQL_DATABASE=statesman_test
|
|
47
|
+
steps: *steps
|
|
48
|
+
build-ruby249-rails-524-postgres:
|
|
49
|
+
docker:
|
|
50
|
+
- image: circleci/ruby:2.4.9-node
|
|
51
|
+
environment:
|
|
52
|
+
- RAILS_VERSION=5.2.4
|
|
53
|
+
- DATABASE_URL=postgres://postgres@localhost/statesman_test
|
|
54
|
+
- DATABASE_DEPENDENCY_PORT=5432
|
|
55
|
+
- image: circleci/postgres:9.6
|
|
56
|
+
environment:
|
|
57
|
+
- POSTGRES_USER=postgres
|
|
58
|
+
- POSTGRES_DB=statesman_test
|
|
59
|
+
- POSTGRES_PASSWORD=statesman
|
|
60
|
+
steps: *steps
|
|
61
|
+
|
|
34
62
|
build-ruby265-rails-602-mysql:
|
|
35
63
|
docker:
|
|
36
64
|
- image: circleci/ruby:2.6.5-node
|
|
@@ -56,6 +84,7 @@ jobs:
|
|
|
56
84
|
environment:
|
|
57
85
|
- POSTGRES_USER=postgres
|
|
58
86
|
- POSTGRES_DB=statesman_test
|
|
87
|
+
- POSTGRES_PASSWORD=statesman
|
|
59
88
|
steps: *steps
|
|
60
89
|
build-ruby265-rails-master-mysql:
|
|
61
90
|
docker:
|
|
@@ -83,12 +112,14 @@ jobs:
|
|
|
83
112
|
environment:
|
|
84
113
|
- POSTGRES_USER=postgres
|
|
85
114
|
- POSTGRES_DB=statesman_test
|
|
115
|
+
- POSTGRES_PASSWORD=statesman
|
|
86
116
|
steps: *steps
|
|
87
|
-
|
|
117
|
+
|
|
118
|
+
build-ruby270-rails-602-mysql:
|
|
88
119
|
docker:
|
|
89
|
-
- image: circleci/ruby:2.
|
|
120
|
+
- image: circleci/ruby:2.7.0-node
|
|
90
121
|
environment:
|
|
91
|
-
- RAILS_VERSION=
|
|
122
|
+
- RAILS_VERSION=6.0.2
|
|
92
123
|
- DATABASE_URL=mysql2://root@127.0.0.1/statesman_test
|
|
93
124
|
- DATABASE_DEPENDENCY_PORT=3306
|
|
94
125
|
- image: circleci/mysql:5.7.18
|
|
@@ -98,19 +129,47 @@ jobs:
|
|
|
98
129
|
- MYSQL_PASSWORD=
|
|
99
130
|
- MYSQL_DATABASE=statesman_test
|
|
100
131
|
steps: *steps
|
|
101
|
-
build-
|
|
132
|
+
build-ruby270-rails-602-postgres:
|
|
102
133
|
docker:
|
|
103
|
-
- image: circleci/ruby:2.
|
|
134
|
+
- image: circleci/ruby:2.7.0-node
|
|
104
135
|
environment:
|
|
105
|
-
- RAILS_VERSION=
|
|
136
|
+
- RAILS_VERSION=6.0.2
|
|
106
137
|
- DATABASE_URL=postgres://postgres@localhost/statesman_test
|
|
107
138
|
- DATABASE_DEPENDENCY_PORT=5432
|
|
108
139
|
- image: circleci/postgres:9.6
|
|
109
140
|
environment:
|
|
110
141
|
- POSTGRES_USER=postgres
|
|
111
142
|
- POSTGRES_DB=statesman_test
|
|
143
|
+
- POSTGRES_PASSWORD=statesman
|
|
144
|
+
steps: *steps
|
|
145
|
+
build-ruby270-rails-master-mysql:
|
|
146
|
+
docker:
|
|
147
|
+
- image: circleci/ruby:2.7.0-node
|
|
148
|
+
environment:
|
|
149
|
+
- RAILS_VERSION=master
|
|
150
|
+
- DATABASE_URL=mysql2://root@127.0.0.1/statesman_test
|
|
151
|
+
- DATABASE_DEPENDENCY_PORT=3306
|
|
152
|
+
- image: circleci/mysql:5.7.18
|
|
153
|
+
environment:
|
|
154
|
+
- MYSQL_ALLOW_EMPTY_PASSWORD=true
|
|
155
|
+
- MYSQL_USER=root
|
|
156
|
+
- MYSQL_PASSWORD=
|
|
157
|
+
- MYSQL_DATABASE=statesman_test
|
|
158
|
+
steps: *steps
|
|
159
|
+
build-ruby270-rails-master-postgres:
|
|
160
|
+
docker:
|
|
161
|
+
- image: circleci/ruby:2.7.0-node
|
|
162
|
+
environment:
|
|
163
|
+
- RAILS_VERSION=master
|
|
164
|
+
- DATABASE_URL=postgres://postgres@localhost/statesman_test
|
|
165
|
+
- EXCLUDE_MONGOID=true
|
|
166
|
+
- DATABASE_DEPENDENCY_PORT=5432
|
|
167
|
+
- image: circleci/postgres:9.6
|
|
168
|
+
environment:
|
|
169
|
+
- POSTGRES_USER=postgres
|
|
170
|
+
- POSTGRES_DB=statesman_test
|
|
171
|
+
- POSTGRES_PASSWORD=statesman
|
|
112
172
|
steps: *steps
|
|
113
|
-
|
|
114
173
|
|
|
115
174
|
workflows:
|
|
116
175
|
version: 2
|
|
@@ -122,3 +181,7 @@ workflows:
|
|
|
122
181
|
- build-ruby265-rails-602-postgres
|
|
123
182
|
- build-ruby265-rails-master-mysql
|
|
124
183
|
- build-ruby265-rails-master-postgres
|
|
184
|
+
- build-ruby270-rails-602-mysql
|
|
185
|
+
- build-ruby270-rails-602-postgres
|
|
186
|
+
- build-ruby270-rails-master-mysql
|
|
187
|
+
- build-ruby270-rails-master-postgres
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
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
|
+
|
|
28
|
+
## v7.0.1, 8th Jan 2020
|
|
29
|
+
|
|
30
|
+
- Fix deprecation warning with Ruby 2.7 [#386](https://github.com/gocardless/statesman/pull/386)
|
|
31
|
+
|
|
1
32
|
## v7.0.0, 8th Jan 2020
|
|
2
33
|
|
|
3
34
|
**Breaking changes**
|
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
|
|
@@ -65,19 +69,36 @@ module Statesman
|
|
|
65
69
|
|
|
66
70
|
private
|
|
67
71
|
|
|
72
|
+
# rubocop:disable Metrics/MethodLength
|
|
68
73
|
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)
|
|
74
|
+
transition = transitions_for_parent.build(
|
|
75
|
+
default_transition_attributes(to, metadata),
|
|
76
|
+
)
|
|
76
77
|
|
|
77
78
|
::ActiveRecord::Base.transaction(requires_new: true) do
|
|
78
79
|
@observer.execute(:before, from, to, transition)
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
|
|
81
|
+
if mysql_gaplock_protection?
|
|
82
|
+
# We save the transition first with most_recent falsy, then mark most_recent
|
|
83
|
+
# true after to avoid letting MySQL acquire a next-key lock which can cause
|
|
84
|
+
# deadlocks.
|
|
85
|
+
#
|
|
86
|
+
# To avoid an additional query, we manually adjust the most_recent attribute
|
|
87
|
+
# on our transition assuming that update_most_recents will have set it to true
|
|
88
|
+
|
|
89
|
+
transition.save!
|
|
90
|
+
|
|
91
|
+
unless update_most_recents(transition.id).positive?
|
|
92
|
+
raise ActiveRecord::Rollback, "failed to update most_recent"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
transition.assign_attributes(most_recent: true)
|
|
96
|
+
else
|
|
97
|
+
update_most_recents
|
|
98
|
+
transition.assign_attributes(most_recent: true)
|
|
99
|
+
transition.save!
|
|
100
|
+
end
|
|
101
|
+
|
|
81
102
|
@last_transition = transition
|
|
82
103
|
@observer.execute(:after, from, to, transition)
|
|
83
104
|
add_after_commit_callback(from, to, transition)
|
|
@@ -85,6 +106,16 @@ module Statesman
|
|
|
85
106
|
|
|
86
107
|
transition
|
|
87
108
|
end
|
|
109
|
+
# rubocop:enable Metrics/MethodLength
|
|
110
|
+
|
|
111
|
+
def default_transition_attributes(to, metadata)
|
|
112
|
+
{
|
|
113
|
+
to_state: to,
|
|
114
|
+
sort_key: next_sort_key,
|
|
115
|
+
metadata: metadata,
|
|
116
|
+
most_recent: not_most_recent_value(db_cast: false),
|
|
117
|
+
}
|
|
118
|
+
end
|
|
88
119
|
|
|
89
120
|
def add_after_commit_callback(from, to, transition)
|
|
90
121
|
::ActiveRecord::Base.connection.add_transaction_record(
|
|
@@ -98,20 +129,92 @@ module Statesman
|
|
|
98
129
|
parent_model.send(@association_name)
|
|
99
130
|
end
|
|
100
131
|
|
|
101
|
-
|
|
102
|
-
|
|
132
|
+
# Sets the given transition most_recent = t while unsetting the most_recent of any
|
|
133
|
+
# previous transitions.
|
|
134
|
+
def update_most_recents(most_recent_id = nil)
|
|
135
|
+
update = build_arel_manager(::Arel::UpdateManager)
|
|
136
|
+
update.table(transition_table)
|
|
137
|
+
update.where(most_recent_transitions(most_recent_id))
|
|
138
|
+
update.set(build_most_recents_update_all_values(most_recent_id))
|
|
103
139
|
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
140
|
+
# MySQL will validate index constraints across the intermediate result of an
|
|
141
|
+
# update. This means we must order our update to deactivate the previous
|
|
142
|
+
# most_recent before setting the new row to be true.
|
|
143
|
+
update.order(transition_table[:most_recent].desc) if mysql_gaplock_protection?
|
|
144
|
+
|
|
145
|
+
::ActiveRecord::Base.connection.update(update.to_sql)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def most_recent_transitions(most_recent_id = nil)
|
|
149
|
+
if most_recent_id
|
|
150
|
+
transitions_of_parent.and(
|
|
151
|
+
transition_table[:id].eq(most_recent_id).or(
|
|
152
|
+
transition_table[:most_recent].eq(true),
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
else
|
|
156
|
+
transitions_of_parent.and(transition_table[:most_recent].eq(true))
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def transitions_of_parent
|
|
161
|
+
transition_table[parent_join_foreign_key.to_sym].eq(parent_model.id)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Generates update_all Arel values that will touch the updated timestamp (if valid
|
|
165
|
+
# for this model) and set most_recent to true only for the transition with a
|
|
166
|
+
# matching most_recent ID.
|
|
167
|
+
#
|
|
168
|
+
# This is quite nasty, but combines two updates (set all most_recent = f, set
|
|
169
|
+
# current most_recent = t) into one, which helps improve transition performance
|
|
170
|
+
# especially when database latency is significant.
|
|
171
|
+
#
|
|
172
|
+
# The SQL this can help produce looks like:
|
|
173
|
+
#
|
|
174
|
+
# update transitions
|
|
175
|
+
# set most_recent = (case when id = 'PA123' then TRUE else FALSE end)
|
|
176
|
+
# , updated_at = '...'
|
|
177
|
+
# ...
|
|
178
|
+
#
|
|
179
|
+
def build_most_recents_update_all_values(most_recent_id = nil)
|
|
180
|
+
[
|
|
181
|
+
[
|
|
182
|
+
transition_table[:most_recent],
|
|
183
|
+
Arel::Nodes::SqlLiteral.new(most_recent_value(most_recent_id)),
|
|
184
|
+
],
|
|
185
|
+
].tap do |values|
|
|
186
|
+
# Only if we support the updated at timestamps should we add this column to the
|
|
187
|
+
# update
|
|
188
|
+
updated_column, updated_at = updated_column_and_timestamp
|
|
189
|
+
|
|
190
|
+
if updated_column
|
|
191
|
+
values << [
|
|
192
|
+
transition_table[updated_column.to_sym],
|
|
193
|
+
updated_at,
|
|
194
|
+
]
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def most_recent_value(most_recent_id)
|
|
200
|
+
if most_recent_id
|
|
201
|
+
Arel::Nodes::Case.new.
|
|
202
|
+
when(transition_table[:id].eq(most_recent_id)).then(db_true).
|
|
203
|
+
else(not_most_recent_value).to_sql
|
|
113
204
|
else
|
|
114
|
-
|
|
205
|
+
Arel::Nodes::SqlLiteral.new(not_most_recent_value)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Provide a wrapper for constructing an update manager which handles a breaking API
|
|
210
|
+
# change in Arel as we move into Rails >6.0.
|
|
211
|
+
#
|
|
212
|
+
# https://github.com/rails/rails/commit/7508284800f67b4611c767bff9eae7045674b66f
|
|
213
|
+
def build_arel_manager(manager)
|
|
214
|
+
if manager.instance_method(:initialize).arity.zero?
|
|
215
|
+
manager.new
|
|
216
|
+
else
|
|
217
|
+
manager.new(::ActiveRecord::Base)
|
|
115
218
|
end
|
|
116
219
|
end
|
|
117
220
|
|
|
@@ -170,7 +273,8 @@ module Statesman
|
|
|
170
273
|
end
|
|
171
274
|
end
|
|
172
275
|
|
|
173
|
-
|
|
276
|
+
# updated_column_and_timestamp should return [column_name, value]
|
|
277
|
+
def updated_column_and_timestamp
|
|
174
278
|
# TODO: Once we've set expectations that transition classes should conform to
|
|
175
279
|
# the interface of Adapters::ActiveRecordTransition as a breaking change in the
|
|
176
280
|
# next major version, we can stop calling `#respond_to?` first and instead
|
|
@@ -184,21 +288,57 @@ module Statesman
|
|
|
184
288
|
ActiveRecordTransition::DEFAULT_UPDATED_TIMESTAMP_COLUMN
|
|
185
289
|
end
|
|
186
290
|
|
|
187
|
-
|
|
291
|
+
# No updated timestamp column, don't return anything
|
|
292
|
+
return nil if column.nil?
|
|
293
|
+
|
|
294
|
+
[
|
|
295
|
+
column, ::ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
|
|
296
|
+
]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def mysql_gaplock_protection?
|
|
300
|
+
Statesman.mysql_gaplock_protection?
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def db_true
|
|
304
|
+
value = ::ActiveRecord::Base.connection.type_cast(
|
|
305
|
+
true,
|
|
306
|
+
transition_class.columns_hash["most_recent"],
|
|
307
|
+
)
|
|
308
|
+
::ActiveRecord::Base.connection.quote(value)
|
|
309
|
+
end
|
|
188
310
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
311
|
+
def db_false
|
|
312
|
+
value = ::ActiveRecord::Base.connection.type_cast(
|
|
313
|
+
false,
|
|
314
|
+
transition_class.columns_hash["most_recent"],
|
|
315
|
+
)
|
|
316
|
+
::ActiveRecord::Base.connection.quote(value)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def db_null
|
|
320
|
+
Arel::Nodes::SqlLiteral.new("NULL")
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Check whether the `most_recent` column allows null values. If it doesn't, set old
|
|
324
|
+
# records to `false`, otherwise, set them to `NULL`.
|
|
325
|
+
#
|
|
326
|
+
# Some conditioning here is required to support databases that don't support partial
|
|
327
|
+
# indexes. By doing the conditioning on the column, rather than Rails' opinion of
|
|
328
|
+
# whether the database supports partial indexes, we're robust to DBs later adding
|
|
329
|
+
# support for partial indexes.
|
|
330
|
+
def not_most_recent_value(db_cast: true)
|
|
331
|
+
if transition_class.columns_hash["most_recent"].null == false
|
|
332
|
+
return db_cast ? db_false : false
|
|
333
|
+
end
|
|
194
334
|
|
|
195
|
-
|
|
335
|
+
db_cast ? db_null : nil
|
|
196
336
|
end
|
|
197
337
|
end
|
|
198
338
|
|
|
199
339
|
class ActiveRecordAfterCommitWrap
|
|
200
|
-
def initialize
|
|
201
|
-
@callback =
|
|
340
|
+
def initialize(&block)
|
|
341
|
+
@callback = block
|
|
202
342
|
@connection = ::ActiveRecord::Base.connection
|
|
203
343
|
end
|
|
204
344
|
|
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/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) }
|
|
@@ -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)
|
|
@@ -22,10 +24,10 @@ Gem::Specification.new do |spec|
|
|
|
22
24
|
spec.required_ruby_version = ">= 2.2"
|
|
23
25
|
|
|
24
26
|
spec.add_development_dependency "ammeter", "~> 1.1"
|
|
25
|
-
spec.add_development_dependency "bundler", "~> 1.
|
|
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.0
|
|
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-08-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ammeter
|
|
@@ -30,14 +30,14 @@ dependencies:
|
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version:
|
|
33
|
+
version: 2.1.4
|
|
34
34
|
type: :development
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version:
|
|
40
|
+
version: 2.1.4
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: gc_ruboconfig
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -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.1.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
|