statesman 7.3.0 → 8.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +6 -36
- data/CHANGELOG.md +52 -1
- data/Gemfile +2 -2
- data/README.md +9 -3
- data/lib/generators/statesman/generator_helpers.rb +10 -2
- data/lib/statesman/adapters/active_record.rb +19 -11
- data/lib/statesman/adapters/active_record_queries.rb +5 -1
- data/lib/statesman/adapters/memory.rb +4 -0
- data/lib/statesman/machine.rb +4 -0
- data/lib/statesman/version.rb +1 -1
- data/spec/statesman/adapters/active_record_queries_spec.rb +25 -0
- data/spec/statesman/adapters/active_record_spec.rb +38 -0
- data/spec/support/active_record.rb +5 -0
- data/statesman.gemspec +11 -1
- metadata +9 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1b67b01e36452d54ed3a46731198cf31b62bc4b96c658f1555c1246a859a369a
|
4
|
+
data.tar.gz: d6bedd395500f7a079c8a7a3dda16c48f78b532dbd37bf9242644d1fb6f5186c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 573dd79df9f8bbf958cfca8bd2a2477b10904916d43917fb269b41f154bde0f40e8bda641f5984c56772d1aa918bbfd84f8a14be881917370167ee1f3c4d6324
|
7
|
+
data.tar.gz: 9440bfaa20b003060c9dfa152d93c7142cff6ffa6ffc56ae52e966c1f96fdd95bd47d9e411987802bc986c7b6557223ce8696ffe446468735b360ed80d130ca3
|
data/.circleci/config.yml
CHANGED
@@ -86,34 +86,6 @@ jobs:
|
|
86
86
|
- POSTGRES_DB=statesman_test
|
87
87
|
- POSTGRES_PASSWORD=statesman
|
88
88
|
steps: *steps
|
89
|
-
build-ruby265-rails-master-mysql:
|
90
|
-
docker:
|
91
|
-
- image: circleci/ruby:2.6.5-node
|
92
|
-
environment:
|
93
|
-
- RAILS_VERSION=master
|
94
|
-
- DATABASE_URL=mysql2://root@127.0.0.1/statesman_test
|
95
|
-
- DATABASE_DEPENDENCY_PORT=3306
|
96
|
-
- image: circleci/mysql:5.7.18
|
97
|
-
environment:
|
98
|
-
- MYSQL_ALLOW_EMPTY_PASSWORD=true
|
99
|
-
- MYSQL_USER=root
|
100
|
-
- MYSQL_PASSWORD=
|
101
|
-
- MYSQL_DATABASE=statesman_test
|
102
|
-
steps: *steps
|
103
|
-
build-ruby265-rails-master-postgres:
|
104
|
-
docker:
|
105
|
-
- image: circleci/ruby:2.6.5-node
|
106
|
-
environment:
|
107
|
-
- RAILS_VERSION=master
|
108
|
-
- DATABASE_URL=postgres://postgres@localhost/statesman_test
|
109
|
-
- EXCLUDE_MONGOID=true
|
110
|
-
- DATABASE_DEPENDENCY_PORT=5432
|
111
|
-
- image: circleci/postgres:9.6
|
112
|
-
environment:
|
113
|
-
- POSTGRES_USER=postgres
|
114
|
-
- POSTGRES_DB=statesman_test
|
115
|
-
- POSTGRES_PASSWORD=statesman
|
116
|
-
steps: *steps
|
117
89
|
|
118
90
|
build-ruby270-rails-602-mysql:
|
119
91
|
docker:
|
@@ -142,11 +114,11 @@ jobs:
|
|
142
114
|
- POSTGRES_DB=statesman_test
|
143
115
|
- POSTGRES_PASSWORD=statesman
|
144
116
|
steps: *steps
|
145
|
-
build-ruby270-rails-
|
117
|
+
build-ruby270-rails-main-mysql:
|
146
118
|
docker:
|
147
119
|
- image: circleci/ruby:2.7.0-node
|
148
120
|
environment:
|
149
|
-
- RAILS_VERSION=
|
121
|
+
- RAILS_VERSION=main
|
150
122
|
- DATABASE_URL=mysql2://root@127.0.0.1/statesman_test
|
151
123
|
- DATABASE_DEPENDENCY_PORT=3306
|
152
124
|
- image: circleci/mysql:5.7.18
|
@@ -156,11 +128,11 @@ jobs:
|
|
156
128
|
- MYSQL_PASSWORD=
|
157
129
|
- MYSQL_DATABASE=statesman_test
|
158
130
|
steps: *steps
|
159
|
-
build-ruby270-rails-
|
131
|
+
build-ruby270-rails-main-postgres:
|
160
132
|
docker:
|
161
133
|
- image: circleci/ruby:2.7.0-node
|
162
134
|
environment:
|
163
|
-
- RAILS_VERSION=
|
135
|
+
- RAILS_VERSION=main
|
164
136
|
- DATABASE_URL=postgres://postgres@localhost/statesman_test
|
165
137
|
- EXCLUDE_MONGOID=true
|
166
138
|
- DATABASE_DEPENDENCY_PORT=5432
|
@@ -179,9 +151,7 @@ workflows:
|
|
179
151
|
- build-ruby249-rails-524-postgres
|
180
152
|
- build-ruby265-rails-602-mysql
|
181
153
|
- build-ruby265-rails-602-postgres
|
182
|
-
- build-ruby265-rails-master-mysql
|
183
|
-
- build-ruby265-rails-master-postgres
|
184
154
|
- build-ruby270-rails-602-mysql
|
185
155
|
- build-ruby270-rails-602-postgres
|
186
|
-
- build-ruby270-rails-
|
187
|
-
- build-ruby270-rails-
|
156
|
+
- build-ruby270-rails-main-mysql
|
157
|
+
- build-ruby270-rails-main-postgres
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,54 @@
|
|
1
|
+
## v8.0.2 30th March 2021
|
2
|
+
|
3
|
+
### Changed
|
4
|
+
|
5
|
+
- Fixed a bug where the `history` of a model was left in an incorrect state after a transition
|
6
|
+
conflict [#433](https://github.com/gocardless/statesman/pull/433)
|
7
|
+
|
8
|
+
## v8.0.1 20th January 2021
|
9
|
+
|
10
|
+
### Changed
|
11
|
+
|
12
|
+
- Fixed `no implicit conversion of nil into String` error when quoting null values
|
13
|
+
[#427](https://github.com/gocardless/statesman/pull/427)
|
14
|
+
|
15
|
+
## v8.0.0 6th January 2021
|
16
|
+
|
17
|
+
### Added
|
18
|
+
|
19
|
+
- Use AR Arel table to type cast booleans in order to avoid deprecation warning [#421](https://github.com/gocardless/statesman/pull/421)
|
20
|
+
- Support relationships that doesn't use `id` as a Primary Key
|
21
|
+
[#422](https://github.com/gocardless/statesman/pull/422)
|
22
|
+
|
23
|
+
## v7.4.1 11th November 2020
|
24
|
+
|
25
|
+
### Added
|
26
|
+
|
27
|
+
- Add #reset method to state machine and adapter interfaces
|
28
|
+
[#417](https://github.com/gocardless/statesman/pull/417)
|
29
|
+
|
30
|
+
## v7.4.0 26th August 2020
|
31
|
+
|
32
|
+
### Added
|
33
|
+
|
34
|
+
- [Gem Metadata](https://guides.rubygems.org/specification-reference/#metadata)
|
35
|
+
to make finding changes between releases even easier.
|
36
|
+
|
37
|
+
## v7.3.0, 24th August 2020
|
38
|
+
|
39
|
+
### Changed
|
40
|
+
|
41
|
+
- Use correct Arel for null [#409](https://github.com/gocardless/statesman/pull/409)
|
42
|
+
|
43
|
+
## v7.2.0, 19th May 2020
|
44
|
+
|
45
|
+
### Changed
|
46
|
+
|
47
|
+
- Set non-empty password for postgres tests [#398](https://github.com/gocardless/statesman/pull/398)
|
48
|
+
- Handle transitions differently for MySQL [#399](https://github.com/gocardless/statesman/pull/399)
|
49
|
+
- pg requirement from >= 0.18, <= 1.1 to >= 0.18, <= 1.3 [#400](https://github.com/gocardless/statesman/pull/400)
|
50
|
+
- Lazily enable mysql gaplock protection [#402](https://github.com/gocardless/statesman/pull/402)
|
51
|
+
|
1
52
|
## v7.1.0, 10th Feb 2020
|
2
53
|
|
3
54
|
- Fix `to_s` on `TransitionFailedError` & `GuardFailedError`. `.message` and
|
@@ -49,7 +100,7 @@
|
|
49
100
|
to
|
50
101
|
```ruby
|
51
102
|
include Statesman::Adapters::ActiveRecordQueries[
|
52
|
-
initial_state: :
|
103
|
+
initial_state: :initial,
|
53
104
|
transition_class: MyTransition
|
54
105
|
]
|
55
106
|
```
|
data/Gemfile
CHANGED
@@ -5,8 +5,8 @@ source 'https://rubygems.org'
|
|
5
5
|
gemspec
|
6
6
|
|
7
7
|
# rubocop:disable Bundler/DuplicatedGem
|
8
|
-
if ENV['RAILS_VERSION'] == '
|
9
|
-
gem "rails", git: "https://github.com/rails/rails"
|
8
|
+
if ENV['RAILS_VERSION'] == 'main'
|
9
|
+
gem "rails", git: "https://github.com/rails/rails", branch: "main"
|
10
10
|
elsif ENV['RAILS_VERSION']
|
11
11
|
gem "rails", "~> #{ENV['RAILS_VERSION']}"
|
12
12
|
end
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
<p align="center"><img src="
|
1
|
+
<p align="center"><img src="https://user-images.githubusercontent.com/110275/106792848-96e4ee80-664e-11eb-8fd1-16ff24b41eb2.png" alt="Statesman" width="512"></p>
|
2
2
|
|
3
3
|
A statesmanlike state machine library.
|
4
4
|
|
@@ -322,6 +322,10 @@ Machine.successors
|
|
322
322
|
#### `Machine#current_state`
|
323
323
|
Returns the current state based on existing transition objects.
|
324
324
|
|
325
|
+
Takes an optional keyword argument to force a reload of data from the
|
326
|
+
database.
|
327
|
+
e.g `current_state(force_reload: true)`
|
328
|
+
|
325
329
|
#### `Machine#in_state?(:state_1, :state_2, ...)`
|
326
330
|
Returns true if the machine is in any of the given states.
|
327
331
|
|
@@ -404,10 +408,12 @@ Model.in_state(:state_1).or(
|
|
404
408
|
#### Storing the state on the model object
|
405
409
|
|
406
410
|
If you wish to store the model state on the model directly, you can keep it up
|
407
|
-
to date using an `after_transition` hook
|
411
|
+
to date using an `after_transition` hook.
|
412
|
+
Combine it with the `after_commit` option to ensure the model state will only be
|
413
|
+
saved once the transition has made it irreversibly to the database:
|
408
414
|
|
409
415
|
```ruby
|
410
|
-
after_transition do |model, transition|
|
416
|
+
after_transition(after_commit: true) do |model, transition|
|
411
417
|
model.state = transition.to_state
|
412
418
|
model.save!
|
413
419
|
end
|
@@ -39,8 +39,16 @@ module Statesman
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def mysql?
|
42
|
-
|
43
|
-
|
42
|
+
configuration.try(:[], "adapter").try(:match, /mysql/)
|
43
|
+
end
|
44
|
+
|
45
|
+
# [] is deprecated and will be removed in 6.2
|
46
|
+
def configuration
|
47
|
+
if ActiveRecord::Base.configurations.respond_to?(:configs_for)
|
48
|
+
ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).first
|
49
|
+
else
|
50
|
+
ActiveRecord::Base.configurations[Rails.env]
|
51
|
+
end
|
44
52
|
end
|
45
53
|
|
46
54
|
def database_supports_partial_indexes?
|
@@ -42,7 +42,13 @@ module Statesman
|
|
42
42
|
def create(from, to, metadata = {})
|
43
43
|
create_transition(from.to_s, to.to_s, metadata)
|
44
44
|
rescue ::ActiveRecord::RecordNotUnique => e
|
45
|
-
|
45
|
+
if transition_conflict_error? e
|
46
|
+
# The history has the invalid transition on the end of it, which means
|
47
|
+
# `current_state` would then be incorrect. We force a reload of the history to
|
48
|
+
# avoid this.
|
49
|
+
transitions_for_parent.reload
|
50
|
+
raise TransitionConflictError, e.message
|
51
|
+
end
|
46
52
|
|
47
53
|
raise
|
48
54
|
ensure
|
@@ -67,6 +73,10 @@ module Statesman
|
|
67
73
|
end
|
68
74
|
end
|
69
75
|
|
76
|
+
def reset
|
77
|
+
@last_transition = nil
|
78
|
+
end
|
79
|
+
|
70
80
|
private
|
71
81
|
|
72
82
|
# rubocop:disable Metrics/MethodLength
|
@@ -301,25 +311,23 @@ module Statesman
|
|
301
311
|
end
|
302
312
|
|
303
313
|
def db_true
|
304
|
-
|
305
|
-
true,
|
306
|
-
transition_class.columns_hash["most_recent"],
|
307
|
-
)
|
308
|
-
::ActiveRecord::Base.connection.quote(value)
|
314
|
+
::ActiveRecord::Base.connection.quote(type_cast(true))
|
309
315
|
end
|
310
316
|
|
311
317
|
def db_false
|
312
|
-
|
313
|
-
false,
|
314
|
-
transition_class.columns_hash["most_recent"],
|
315
|
-
)
|
316
|
-
::ActiveRecord::Base.connection.quote(value)
|
318
|
+
::ActiveRecord::Base.connection.quote(type_cast(false))
|
317
319
|
end
|
318
320
|
|
319
321
|
def db_null
|
320
322
|
Arel::Nodes::SqlLiteral.new("NULL")
|
321
323
|
end
|
322
324
|
|
325
|
+
# Type casting against a column is deprecated and will be removed in Rails 6.2.
|
326
|
+
# See https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11
|
327
|
+
def type_cast(value)
|
328
|
+
::ActiveRecord::Base.connection.type_cast(value)
|
329
|
+
end
|
330
|
+
|
323
331
|
# Check whether the `most_recent` column allows null values. If it doesn't, set old
|
324
332
|
# records to `false`, otherwise, set them to `NULL`.
|
325
333
|
#
|
@@ -104,7 +104,7 @@ module Statesman
|
|
104
104
|
|
105
105
|
def most_recent_transition_join
|
106
106
|
"LEFT OUTER JOIN #{model_table} AS #{most_recent_transition_alias} " \
|
107
|
-
"ON #{model.table_name}
|
107
|
+
"ON #{model.table_name}.#{model_primary_key} = " \
|
108
108
|
"#{most_recent_transition_alias}.#{model_foreign_key} " \
|
109
109
|
"AND #{most_recent_transition_alias}.most_recent = #{db_true}"
|
110
110
|
end
|
@@ -127,6 +127,10 @@ module Statesman
|
|
127
127
|
"and #{transition_class}."
|
128
128
|
end
|
129
129
|
|
130
|
+
def model_primary_key
|
131
|
+
transition_reflection.active_record_primary_key
|
132
|
+
end
|
133
|
+
|
130
134
|
def model_foreign_key
|
131
135
|
transition_reflection.foreign_key
|
132
136
|
end
|
data/lib/statesman/machine.rb
CHANGED
data/lib/statesman/version.rb
CHANGED
@@ -154,6 +154,31 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
154
154
|
end
|
155
155
|
end
|
156
156
|
|
157
|
+
context "with a custom primary key for the model" do
|
158
|
+
before do
|
159
|
+
# Switch to using OtherActiveRecordModelTransition, so the existing
|
160
|
+
# relation with MyActiveRecordModelTransition doesn't interfere with
|
161
|
+
# this spec.
|
162
|
+
# Configure the relationship to use a different primary key,
|
163
|
+
MyActiveRecordModel.send(:has_many,
|
164
|
+
:custom_name,
|
165
|
+
class_name: "OtherActiveRecordModelTransition",
|
166
|
+
primary_key: :external_id)
|
167
|
+
|
168
|
+
MyActiveRecordModel.class_eval do
|
169
|
+
def self.transition_class
|
170
|
+
OtherActiveRecordModelTransition
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe ".in_state" do
|
176
|
+
subject(:query) { MyActiveRecordModel.in_state(:succeeded) }
|
177
|
+
|
178
|
+
specify { expect { query }.to_not raise_error }
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
157
182
|
context "after_commit transactional integrity" do
|
158
183
|
before do
|
159
184
|
MyStateMachine.class_eval do
|
@@ -130,6 +130,31 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
130
130
|
expect { adapter.create(:y, :z) }.
|
131
131
|
to raise_exception(Statesman::TransitionConflictError)
|
132
132
|
end
|
133
|
+
|
134
|
+
it "does not pollute the state when the transition fails" do
|
135
|
+
# this increments the sort_key in the database
|
136
|
+
adapter.create(:x, :y)
|
137
|
+
|
138
|
+
# we then pre-load the transitions for efficiency
|
139
|
+
preloaded_model = MyActiveRecordModel.
|
140
|
+
includes(:my_active_record_model_transitions).
|
141
|
+
find(model.id)
|
142
|
+
|
143
|
+
adapter2 = described_class.
|
144
|
+
new(MyActiveRecordModelTransition, preloaded_model, observer)
|
145
|
+
|
146
|
+
# Now we generate a race
|
147
|
+
adapter.create(:y, :z)
|
148
|
+
expect { adapter2.create(:y, :a) }.
|
149
|
+
to raise_error(Statesman::TransitionConflictError)
|
150
|
+
|
151
|
+
# The preloaded adapter should discard the preloaded info
|
152
|
+
expect(adapter2.last).to have_attributes(to_state: "z")
|
153
|
+
expect(adapter2.history).to contain_exactly(
|
154
|
+
have_attributes(to_state: "y"),
|
155
|
+
have_attributes(to_state: "z"),
|
156
|
+
)
|
157
|
+
end
|
133
158
|
end
|
134
159
|
|
135
160
|
context "when other exceptions occur" do
|
@@ -355,6 +380,19 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
355
380
|
end
|
356
381
|
end
|
357
382
|
|
383
|
+
it "resets last with #reload" do
|
384
|
+
model.save!
|
385
|
+
ActiveRecord::Base.transaction do
|
386
|
+
model.state_machine.transition_to!(:succeeded)
|
387
|
+
# force to cache value in last_transition instance variable
|
388
|
+
expect(model.state_machine.current_state).to eq("succeeded")
|
389
|
+
raise ActiveRecord::Rollback
|
390
|
+
end
|
391
|
+
expect(model.state_machine.current_state).to eq("succeeded")
|
392
|
+
model.reload
|
393
|
+
expect(model.state_machine.current_state).to eq("initial")
|
394
|
+
end
|
395
|
+
|
358
396
|
context "with a namespaced model" do
|
359
397
|
before do
|
360
398
|
CreateNamespacedARModelMigration.migrate(:up)
|
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)
|
@@ -35,4 +37,12 @@ Gem::Specification.new do |spec|
|
|
35
37
|
spec.add_development_dependency "rspec_junit_formatter", "~> 0.4.0"
|
36
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:
|
4
|
+
version: 8.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GoCardless
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-03-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ammeter
|
@@ -282,7 +282,12 @@ files:
|
|
282
282
|
homepage: https://github.com/gocardless/statesman
|
283
283
|
licenses:
|
284
284
|
- MIT
|
285
|
-
metadata:
|
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
|
286
291
|
post_install_message:
|
287
292
|
rdoc_options: []
|
288
293
|
require_paths:
|
@@ -298,7 +303,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
298
303
|
- !ruby/object:Gem::Version
|
299
304
|
version: '0'
|
300
305
|
requirements: []
|
301
|
-
rubygems_version: 3.1.
|
306
|
+
rubygems_version: 3.1.2
|
302
307
|
signing_key:
|
303
308
|
specification_version: 4
|
304
309
|
summary: A statesman-like state machine library
|