statesman 7.3.0 → 8.0.2

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