statesman 10.2.3 → 12.1.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/tests.yml +17 -11
  3. data/.gitignore +0 -3
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +1 -1
  6. data/.ruby-version +1 -1
  7. data/CHANGELOG.md +183 -43
  8. data/CONTRIBUTING.md +14 -13
  9. data/Gemfile +2 -2
  10. data/README.md +120 -62
  11. data/docs/COMPATIBILITY.md +2 -2
  12. data/lib/generators/statesman/generator_helpers.rb +1 -1
  13. data/lib/statesman/adapters/active_record.rb +26 -33
  14. data/lib/statesman/adapters/active_record_queries.rb +2 -2
  15. data/lib/statesman/adapters/active_record_transition.rb +5 -1
  16. data/lib/statesman/callback.rb +2 -2
  17. data/lib/statesman/config.rb +3 -10
  18. data/lib/statesman/machine.rb +8 -0
  19. data/lib/statesman/version.rb +1 -1
  20. data/lib/statesman.rb +3 -5
  21. data/lib/tasks/statesman.rake +2 -2
  22. data/spec/generators/statesman/active_record_transition_generator_spec.rb +7 -1
  23. data/spec/generators/statesman/migration_generator_spec.rb +5 -1
  24. data/spec/spec_helper.rb +34 -8
  25. data/spec/statesman/adapters/active_record_queries_spec.rb +1 -3
  26. data/spec/statesman/adapters/active_record_spec.rb +58 -39
  27. data/spec/statesman/adapters/active_record_transition_spec.rb +5 -2
  28. data/spec/statesman/adapters/memory_spec.rb +0 -1
  29. data/spec/statesman/adapters/memory_transition_spec.rb +0 -1
  30. data/spec/statesman/adapters/shared_examples.rb +0 -2
  31. data/spec/statesman/adapters/type_safe_active_record_queries_spec.rb +1 -3
  32. data/spec/statesman/callback_spec.rb +0 -2
  33. data/spec/statesman/config_spec.rb +0 -2
  34. data/spec/statesman/exceptions_spec.rb +1 -3
  35. data/spec/statesman/guard_spec.rb +0 -2
  36. data/spec/statesman/machine_spec.rb +71 -2
  37. data/spec/statesman/utils_spec.rb +0 -2
  38. data/spec/support/active_record.rb +55 -13
  39. data/spec/support/exactly_query_databases.rb +35 -0
  40. data/statesman.gemspec +5 -5
  41. metadata +14 -12
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
3
  describe Statesman::Config do
6
4
  let(:instance) { described_class.new }
7
5
 
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
- describe Statesman do
3
+ describe "Exceptions" do
6
4
  describe "InvalidStateError" do
7
5
  subject(:error) { Statesman::InvalidStateError.new }
8
6
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
3
  describe Statesman::Guard do
6
4
  let(:callback) { -> {} }
7
5
  let(:guard) { described_class.new(from: nil, to: nil, callback: callback) }
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
3
  describe Statesman::Machine do
6
4
  let(:machine) { Class.new { include Statesman::Machine } }
7
5
  let(:my_model) { Class.new { attr_accessor :current_state }.new }
@@ -480,12 +478,83 @@ describe Statesman::Machine do
480
478
  it_behaves_like "a callback store", :after_guard_failure, :after_guard_failure
481
479
  end
482
480
 
481
+ shared_examples "initial transition is not created" do
482
+ it "doesn't call .create on storage adapter" do
483
+ expect_any_instance_of(Statesman.storage_adapter).to_not receive(:create)
484
+ machine.new(my_model, options)
485
+ end
486
+ end
487
+
488
+ shared_examples "initial transition is created" do
489
+ it "calls .create on storage adapter" do
490
+ expect_any_instance_of(Statesman.storage_adapter).to receive(:create).with(nil, "x")
491
+ machine.new(my_model, options)
492
+ end
493
+
494
+ it "creates a new transition object" do
495
+ instance = machine.new(my_model, options)
496
+
497
+ expect(instance.history.count).to eq(1)
498
+ expect(instance.history.first.to_state).to eq("x")
499
+ end
500
+ end
501
+
483
502
  describe "#initialize" do
484
503
  it "accepts an object to manipulate" do
485
504
  machine_instance = machine.new(my_model)
486
505
  expect(machine_instance.object).to be(my_model)
487
506
  end
488
507
 
508
+ context "initial_transition is not provided" do
509
+ let(:options) { {} }
510
+
511
+ it_behaves_like "initial transition is not created"
512
+ end
513
+
514
+ context "initial_transition is provided" do
515
+ context "initial_transition is true" do
516
+ let(:options) do
517
+ { initial_transition: true,
518
+ transition_class: Statesman::Adapters::MemoryTransition }
519
+ end
520
+
521
+ context "history is empty" do
522
+ context "initial state is defined" do
523
+ before { machine.state(:x, initial: true) }
524
+
525
+ it_behaves_like "initial transition is created"
526
+ end
527
+
528
+ context "initial state is not defined" do
529
+ it_behaves_like "initial transition is not created"
530
+ end
531
+ end
532
+
533
+ context "history is not empty" do
534
+ before do
535
+ allow_any_instance_of(Statesman.storage_adapter).to receive(:history).
536
+ and_return([{}])
537
+ end
538
+
539
+ context "initial state is defined" do
540
+ before { machine.state(:x, initial: true) }
541
+
542
+ it_behaves_like "initial transition is not created"
543
+ end
544
+
545
+ context "initial state is not defined" do
546
+ it_behaves_like "initial transition is not created"
547
+ end
548
+ end
549
+ end
550
+
551
+ context "initial_transition is false" do
552
+ let(:options) { { initial_transition: false } }
553
+
554
+ it_behaves_like "initial transition is not created"
555
+ end
556
+ end
557
+
489
558
  context "transition class" do
490
559
  it "sets a default" do
491
560
  expect(Statesman.storage_adapter).to receive(:new).once.
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
4
-
5
3
  describe Statesman::Utils do
6
4
  describe ".rails_major_version" do
7
5
  subject { described_class.rails_major_version }
@@ -24,7 +24,6 @@ class MyActiveRecordModelTransition < ActiveRecord::Base
24
24
  include Statesman::Adapters::ActiveRecordTransition
25
25
 
26
26
  belongs_to :my_active_record_model
27
- serialize :metadata, JSON
28
27
  end
29
28
 
30
29
  class MyActiveRecordModel < ActiveRecord::Base
@@ -51,7 +50,11 @@ class MyActiveRecordModelTransitionWithoutInclude < ActiveRecord::Base
51
50
  self.table_name = "my_active_record_model_transitions"
52
51
 
53
52
  belongs_to :my_active_record_model
54
- serialize :metadata, JSON
53
+ if ::ActiveRecord.gem_version >= Gem::Version.new("7.1")
54
+ serialize :metadata, coder: JSON
55
+ else
56
+ serialize :metadata, JSON
57
+ end
55
58
  end
56
59
 
57
60
  class CreateMyActiveRecordModelMigration < MIGRATION_CLASS
@@ -78,7 +81,7 @@ class CreateMyActiveRecordModelTransitionMigration < MIGRATION_CLASS
78
81
  t.text :metadata, default: "{}"
79
82
  end
80
83
 
81
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
84
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base)
82
85
  t.boolean :most_recent, default: true, null: false
83
86
  else
84
87
  t.boolean :most_recent, default: true
@@ -95,7 +98,7 @@ class CreateMyActiveRecordModelTransitionMigration < MIGRATION_CLASS
95
98
  %i[my_active_record_model_id sort_key],
96
99
  unique: true, name: "sort_key_index"
97
100
 
98
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
101
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base)
99
102
  add_index :my_active_record_model_transitions,
100
103
  %i[my_active_record_model_id most_recent],
101
104
  unique: true,
@@ -129,7 +132,48 @@ class OtherActiveRecordModelTransition < ActiveRecord::Base
129
132
  include Statesman::Adapters::ActiveRecordTransition
130
133
 
131
134
  belongs_to :other_active_record_model
132
- serialize :metadata, JSON
135
+ end
136
+
137
+ class SecondaryRecord < ActiveRecord::Base
138
+ self.abstract_class = true
139
+
140
+ connects_to database: { writing: :secondary, reading: :secondary }
141
+ end
142
+
143
+ class SecondaryActiveRecordModelTransition < SecondaryRecord
144
+ self.table_name = "my_active_record_model_transitions"
145
+
146
+ include Statesman::Adapters::ActiveRecordTransition
147
+
148
+ belongs_to :my_active_record_model,
149
+ class_name: "SecondaryActiveRecordModel",
150
+ foreign_key: "my_active_record_model_transition_id"
151
+ end
152
+
153
+ class SecondaryActiveRecordModel < SecondaryRecord
154
+ self.table_name = "my_active_record_models"
155
+
156
+ has_many :my_active_record_model_transitions,
157
+ class_name: "SecondaryActiveRecordModelTransition",
158
+ foreign_key: "my_active_record_model_id",
159
+ autosave: false
160
+
161
+ alias_method :transitions, :my_active_record_model_transitions
162
+
163
+ include Statesman::Adapters::ActiveRecordQueries[
164
+ transition_class: SecondaryActiveRecordModelTransition,
165
+ initial_state: :initial
166
+ ]
167
+
168
+ def state_machine
169
+ @state_machine ||= MyStateMachine.new(
170
+ self, transition_class: SecondaryActiveRecordModelTransition
171
+ )
172
+ end
173
+
174
+ def metadata
175
+ super || {}
176
+ end
133
177
  end
134
178
 
135
179
  class CreateOtherActiveRecordModelMigration < MIGRATION_CLASS
@@ -156,7 +200,7 @@ class CreateOtherActiveRecordModelTransitionMigration < MIGRATION_CLASS
156
200
  t.text :metadata, default: "{}"
157
201
  end
158
202
 
159
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
203
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base)
160
204
  t.boolean :most_recent, default: true, null: false
161
205
  else
162
206
  t.boolean :most_recent, default: true
@@ -169,7 +213,7 @@ class CreateOtherActiveRecordModelTransitionMigration < MIGRATION_CLASS
169
213
  %i[other_active_record_model_id sort_key],
170
214
  unique: true, name: "other_sort_key_index"
171
215
 
172
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
216
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base)
173
217
  add_index :other_active_record_model_transitions,
174
218
  %i[other_active_record_model_id most_recent],
175
219
  unique: true,
@@ -221,7 +265,6 @@ module MyNamespace
221
265
 
222
266
  belongs_to :my_active_record_model,
223
267
  class_name: "MyNamespace::MyActiveRecordModel"
224
- serialize :metadata, JSON
225
268
 
226
269
  def self.table_name_prefix
227
270
  "my_namespace_"
@@ -252,7 +295,7 @@ class CreateNamespacedARModelTransitionMigration < MIGRATION_CLASS
252
295
  t.text :metadata, default: "{}"
253
296
  end
254
297
 
255
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
298
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base)
256
299
  t.boolean :most_recent, default: true, null: false
257
300
  else
258
301
  t.boolean :most_recent, default: true
@@ -264,7 +307,7 @@ class CreateNamespacedARModelTransitionMigration < MIGRATION_CLASS
264
307
  add_index :my_namespace_my_active_record_model_transitions, :sort_key,
265
308
  unique: true, name: "my_namespaced_key"
266
309
 
267
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
310
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base)
268
311
  add_index :my_namespace_my_active_record_model_transitions,
269
312
  %i[my_active_record_model_id most_recent],
270
313
  unique: true,
@@ -310,7 +353,6 @@ class StiActiveRecordModelTransition < ActiveRecord::Base
310
353
  include Statesman::Adapters::ActiveRecordTransition
311
354
 
312
355
  belongs_to :sti_active_record_model
313
- serialize :metadata, JSON
314
356
  end
315
357
 
316
358
  class StiAActiveRecordModelTransition < StiActiveRecordModelTransition
@@ -342,7 +384,7 @@ class CreateStiActiveRecordModelTransitionMigration < MIGRATION_CLASS
342
384
  t.text :metadata, default: "{}"
343
385
  end
344
386
 
345
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
387
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base)
346
388
  t.boolean :most_recent, default: true, null: false
347
389
  else
348
390
  t.boolean :most_recent, default: true
@@ -355,7 +397,7 @@ class CreateStiActiveRecordModelTransitionMigration < MIGRATION_CLASS
355
397
  %i[type sti_active_record_model_id sort_key],
356
398
  unique: true, name: "sti_sort_key_index"
357
399
 
358
- if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
400
+ if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?(ActiveRecord::Base)
359
401
  add_index :sti_active_record_model_transitions,
360
402
  %i[type sti_active_record_model_id most_recent],
361
403
  unique: true,
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # `expected_dbs` should be a Hash of the form:
4
+ # {
5
+ # primary: [:writing, :reading],
6
+ # replica: [:reading],
7
+ # }
8
+ RSpec::Matchers.define :exactly_query_databases do |expected_dbs|
9
+ match do |block|
10
+ @expected_dbs = expected_dbs.transform_values(&:to_set).with_indifferent_access
11
+ @actual_dbs = Hash.new { |h, k| h[k] = Set.new }.with_indifferent_access
12
+
13
+ ActiveSupport::Notifications.
14
+ subscribe("sql.active_record") do |_name, _start, _finish, _id, payload|
15
+ pool = payload.fetch(:connection).pool
16
+
17
+ next if pool.is_a?(ActiveRecord::ConnectionAdapters::NullPool)
18
+
19
+ name = pool.db_config.name
20
+ role = pool.role
21
+
22
+ @actual_dbs[name] << role
23
+ end
24
+
25
+ block.call
26
+
27
+ @actual_dbs == @expected_dbs
28
+ end
29
+
30
+ failure_message do |_block|
31
+ "expected to query exactly #{@expected_dbs}, but queried #{@actual_dbs}"
32
+ end
33
+
34
+ supports_block_expectations
35
+ end
data/statesman.gemspec CHANGED
@@ -20,20 +20,20 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ["lib"]
22
22
 
23
- spec.required_ruby_version = ">= 2.7"
23
+ spec.required_ruby_version = ">= 3.0"
24
24
 
25
25
  spec.add_development_dependency "ammeter", "~> 1.1"
26
26
  spec.add_development_dependency "bundler", "~> 2"
27
- spec.add_development_dependency "gc_ruboconfig", "~> 3.6.0"
27
+ spec.add_development_dependency "gc_ruboconfig", "~> 4.4.1"
28
28
  spec.add_development_dependency "mysql2", ">= 0.4", "< 0.6"
29
- spec.add_development_dependency "pg", ">= 0.18", "<= 1.5"
29
+ spec.add_development_dependency "pg", ">= 0.18", "<= 1.6"
30
30
  spec.add_development_dependency "rails", ">= 5.2"
31
- spec.add_development_dependency "rake", "~> 13.0.0"
31
+ spec.add_development_dependency "rake", "~> 13.1.0"
32
32
  spec.add_development_dependency "rspec", "~> 3.1"
33
33
  spec.add_development_dependency "rspec-github", "~> 2.4.0"
34
34
  spec.add_development_dependency "rspec-its", "~> 1.1"
35
35
  spec.add_development_dependency "rspec-rails", "~> 6.0"
36
- spec.add_development_dependency "sqlite3", "~> 1.6.1"
36
+ spec.add_development_dependency "sqlite3", "~> 1.7.0"
37
37
  spec.add_development_dependency "timecop", "~> 0.9.1"
38
38
 
39
39
  spec.metadata = {
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: 10.2.3
4
+ version: 12.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GoCardless
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-04 00:00:00.000000000 Z
11
+ date: 2024-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ammeter
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 3.6.0
47
+ version: 4.4.1
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 3.6.0
54
+ version: 4.4.1
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: mysql2
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -81,7 +81,7 @@ dependencies:
81
81
  version: '0.18'
82
82
  - - "<="
83
83
  - !ruby/object:Gem::Version
84
- version: '1.5'
84
+ version: '1.6'
85
85
  type: :development
86
86
  prerelease: false
87
87
  version_requirements: !ruby/object:Gem::Requirement
@@ -91,7 +91,7 @@ dependencies:
91
91
  version: '0.18'
92
92
  - - "<="
93
93
  - !ruby/object:Gem::Version
94
- version: '1.5'
94
+ version: '1.6'
95
95
  - !ruby/object:Gem::Dependency
96
96
  name: rails
97
97
  requirement: !ruby/object:Gem::Requirement
@@ -112,14 +112,14 @@ dependencies:
112
112
  requirements:
113
113
  - - "~>"
114
114
  - !ruby/object:Gem::Version
115
- version: 13.0.0
115
+ version: 13.1.0
116
116
  type: :development
117
117
  prerelease: false
118
118
  version_requirements: !ruby/object:Gem::Requirement
119
119
  requirements:
120
120
  - - "~>"
121
121
  - !ruby/object:Gem::Version
122
- version: 13.0.0
122
+ version: 13.1.0
123
123
  - !ruby/object:Gem::Dependency
124
124
  name: rspec
125
125
  requirement: !ruby/object:Gem::Requirement
@@ -182,14 +182,14 @@ dependencies:
182
182
  requirements:
183
183
  - - "~>"
184
184
  - !ruby/object:Gem::Version
185
- version: 1.6.1
185
+ version: 1.7.0
186
186
  type: :development
187
187
  prerelease: false
188
188
  version_requirements: !ruby/object:Gem::Requirement
189
189
  requirements:
190
190
  - - "~>"
191
191
  - !ruby/object:Gem::Version
192
- version: 1.6.1
192
+ version: 1.7.0
193
193
  - !ruby/object:Gem::Dependency
194
194
  name: timecop
195
195
  requirement: !ruby/object:Gem::Requirement
@@ -214,6 +214,7 @@ files:
214
214
  - ".github/dependabot.yml"
215
215
  - ".github/workflows/tests.yml"
216
216
  - ".gitignore"
217
+ - ".rspec"
217
218
  - ".rubocop.yml"
218
219
  - ".rubocop_todo.yml"
219
220
  - ".ruby-version"
@@ -267,6 +268,7 @@ files:
267
268
  - spec/statesman/machine_spec.rb
268
269
  - spec/statesman/utils_spec.rb
269
270
  - spec/support/active_record.rb
271
+ - spec/support/exactly_query_databases.rb
270
272
  - spec/support/generators_shared_examples.rb
271
273
  - statesman.gemspec
272
274
  homepage: https://github.com/gocardless/statesman
@@ -287,14 +289,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
287
289
  requirements:
288
290
  - - ">="
289
291
  - !ruby/object:Gem::Version
290
- version: '2.7'
292
+ version: '3.0'
291
293
  required_rubygems_version: !ruby/object:Gem::Requirement
292
294
  requirements:
293
295
  - - ">="
294
296
  - !ruby/object:Gem::Version
295
297
  version: '0'
296
298
  requirements: []
297
- rubygems_version: 3.4.1
299
+ rubygems_version: 3.4.10
298
300
  signing_key:
299
301
  specification_version: 4
300
302
  summary: A statesman-like state machine library