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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 444b828dd17ce5984d99e93e25ff774ccbc621a06d0afeb51dc6d8a4b7cd0bc5
4
- data.tar.gz: 8e70e4e74a6edc795d66f203619974c1483d8b2a3b02a19e5264ba588e991a37
3
+ metadata.gz: 1b67b01e36452d54ed3a46731198cf31b62bc4b96c658f1555c1246a859a369a
4
+ data.tar.gz: d6bedd395500f7a079c8a7a3dda16c48f78b532dbd37bf9242644d1fb6f5186c
5
5
  SHA512:
6
- metadata.gz: 9e354a540525c8ab3a2c2f0de4b7c6eb4ecc931f54e1d46dca64f03e910a3f95267f94fb61584444f53e37891ac5a8bd75a2054001eaff118a8a3c5eccad6be3
7
- data.tar.gz: 1f2266d2181d451621ca355c9c5e8db982548f0028c65af723e3d9164deb43f1516e1550fd11678f99b5243e47302ee5fcd916cc2b0f9fb6f7dc9a4a8edb6218
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-master-mysql:
117
+ build-ruby270-rails-main-mysql:
146
118
  docker:
147
119
  - image: circleci/ruby:2.7.0-node
148
120
  environment:
149
- - RAILS_VERSION=master
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-master-postgres:
131
+ build-ruby270-rails-main-postgres:
160
132
  docker:
161
133
  - image: circleci/ruby:2.7.0-node
162
134
  environment:
163
- - RAILS_VERSION=master
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-master-mysql
187
- - build-ruby270-rails-master-postgres
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: :inital,
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'] == 'master'
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="http://f.cl.ly/items/410n2A0S3l1W0i3i0o2K/statesman.png" alt="Statesman"></p>
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
- ActiveRecord::Base.configurations[Rails.env].
43
- try(:[], "adapter").try(:match, /mysql/)
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
- raise TransitionConflictError, e.message if transition_conflict_error? e
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
- value = ::ActiveRecord::Base.connection.type_cast(
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
- value = ::ActiveRecord::Base.connection.type_cast(
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}.id = " \
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
@@ -37,6 +37,10 @@ module Statesman
37
37
  @history
38
38
  end
39
39
 
40
+ def reset
41
+ @history = []
42
+ end
43
+
40
44
  private
41
45
 
42
46
  def next_sort_key
@@ -257,6 +257,10 @@ module Statesman
257
257
  false
258
258
  end
259
259
 
260
+ def reset
261
+ @storage_adapter.reset
262
+ end
263
+
260
264
  private
261
265
 
262
266
  def adapter_class(transition_class)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Statesman
4
- VERSION = "7.3.0"
4
+ VERSION = "8.0.2"
5
5
  end
@@ -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)
@@ -33,6 +33,11 @@ class MyActiveRecordModel < ActiveRecord::Base
33
33
  def metadata
34
34
  super || {}
35
35
  end
36
+
37
+ def reload(*)
38
+ state_machine.reset
39
+ super
40
+ end
36
41
  end
37
42
 
38
43
  class MyActiveRecordModelTransition < ActiveRecord::Base
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 = "https://github.com/gocardless/statesman"
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: 7.3.0
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: 2020-08-24 00:00:00.000000000 Z
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.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