statesman 10.0.0 → 10.2.3

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: 172b3bbeecd2cbd2df8fc9a5485c548c0d729cf0994fda1771d25c8e44f41877
4
- data.tar.gz: 3556cc13e2433e920ce9bd275bce155a095f6a0e64547a5ff003e0f5fa4dfc74
3
+ metadata.gz: 0cc749949e382ced6f27722a0eb033efc0383ba11a06265a84e1da3b593eb02d
4
+ data.tar.gz: f2dd851b5c9ccb0aacbde8076c10fcf23bfe8a00926e58acfb9dd714c5d61b99
5
5
  SHA512:
6
- metadata.gz: 21f91dd2537c3e5e480dc0f5adb592ebbeaef9974cfbede1d94aa8b6946c26e9394199029e0281372a2ec4452e1173cb5cba86f0bf1507de19677c398df20350
7
- data.tar.gz: 58ee880508df011be1caea1f85a475760cf79e07c1ad60f241f4ef4146983997560f17fe1d3a93f5856c6e519831d71eae6c4bd8f7e70f6d0d4ce49253f1ac3f
6
+ metadata.gz: 4ac72394722dc0d193874967e79bb30df1a1c7ba63e4eccd20294f2298b2e78637b3c7ffff7fe4ef4f4ad5d3ca522efe89444400fa09231684622848754a7683
7
+ data.tar.gz: e995d62de4ec4471ec1a66fea6e290bfbc5f0a3420c3bd915117a0e92f08ca20994b59e84ed5f73a93d091156ea47ef35548ea61f50ad52a1942a69438d4f446
@@ -0,0 +1,106 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - "master"
7
+ pull_request:
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ rubocop:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v3
18
+ - uses: ruby/setup-ruby@v1
19
+ with:
20
+ bundler-cache: true
21
+ - run: bundle exec rubocop --extra-details --display-style-guide --parallel --force-exclusion
22
+
23
+ postgres:
24
+ strategy:
25
+ fail-fast: false
26
+ matrix:
27
+ ruby-version: ["2.7", "3.0", "3.1", "3.2"]
28
+ rails-version:
29
+ - "6.1.5"
30
+ - "7.0.4"
31
+ - "main"
32
+ postgres-version: ["9.6", "11", "14"]
33
+ exclude:
34
+ - ruby-version: "3.2"
35
+ rails-version: "6.1.5"
36
+ runs-on: ubuntu-latest
37
+ services:
38
+ postgres:
39
+ image: postgres:${{ matrix.postgres-version }}
40
+ env:
41
+ POSTGRES_USER: postgres
42
+ POSTGRES_DB: statesman_test
43
+ POSTGRES_PASSWORD: statesman
44
+ ports:
45
+ - 5432:5432
46
+ options: >-
47
+ --health-cmd pg_isready
48
+ --health-interval 10s
49
+ --health-timeout 5s
50
+ --health-retries 10
51
+ env:
52
+ DATABASE_URL: postgres://postgres:statesman@localhost/statesman_test
53
+ DATABASE_DEPENDENCY_PORT: "5432"
54
+ steps:
55
+ - uses: actions/checkout@v3
56
+ - name: Set up Ruby
57
+ uses: ruby/setup-ruby@v1
58
+ with:
59
+ bundler-cache: true
60
+ ruby-version: "${{ matrix.ruby-version }}"
61
+ - name: Run specs
62
+ run: |
63
+ bundle exec rspec --profile --format progress --format RSpec::Github::Formatter
64
+
65
+ mysql:
66
+ strategy:
67
+ fail-fast: false
68
+ matrix:
69
+ ruby-version: ["2.7", "3.0", "3.1", "3.2"]
70
+ rails-version:
71
+ - "6.1.5"
72
+ - "7.0.4"
73
+ - "main"
74
+ mysql-version: ["5.7", "8.0"]
75
+ exclude:
76
+ - ruby-version: 3.2
77
+ rails-version: "6.1.5"
78
+ runs-on: ubuntu-latest
79
+ services:
80
+ mysql:
81
+ image: mysql:${{ matrix.mysql-version }}
82
+ env:
83
+ MYSQL_ROOT_PASSWORD: password
84
+ MYSQL_USER: foobar
85
+ MYSQL_PASSWORD: password
86
+ MYSQL_DATABASE: statesman_test
87
+ ports:
88
+ - "3306:3306"
89
+ options: >-
90
+ --health-cmd "mysqladmin ping"
91
+ --health-interval 10s
92
+ --health-timeout 5s
93
+ --health-retries 5
94
+ env:
95
+ DATABASE_URL: mysql2://foobar:password@127.0.0.1/statesman_test
96
+ DATABASE_DEPENDENCY_PORT: "3306"
97
+ steps:
98
+ - uses: actions/checkout@v3
99
+ - name: Set up Ruby
100
+ uses: ruby/setup-ruby@v1
101
+ with:
102
+ bundler-cache: true
103
+ ruby-version: "${{ matrix.ruby-version }}"
104
+ - name: Run specs
105
+ run: |
106
+ bundle exec rspec --profile --format progress --format RSpec::Github::Formatter
data/.rubocop.yml CHANGED
@@ -4,7 +4,8 @@ inherit_gem:
4
4
  gc_ruboconfig: rubocop.yml
5
5
 
6
6
  AllCops:
7
- TargetRubyVersion: 3.0
7
+ TargetRubyVersion: 2.7
8
+ NewCops: enable
8
9
 
9
10
  Metrics/AbcSize:
10
11
  Max: 60
@@ -14,3 +15,6 @@ Metrics/CyclomaticComplexity:
14
15
 
15
16
  Metrics/PerceivedComplexity:
16
17
  Max: 11
18
+
19
+ Gemspec/DevelopmentDependencies:
20
+ Enabled: false
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.0.2
1
+ 3.2.0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,28 @@
1
+ ## v10.2.3 2nd Aug 2023
2
+
3
+ ### Changed
4
+ - Fixed calls to reloading internal cache is the state_machine was made private / protected
5
+
6
+ ## v10.2.2 21st April 2023
7
+
8
+ ### Changed
9
+ - Calling `active_record.reload` resets the adapater's internal cache
10
+
11
+ ## v10.2.1 3rd April 2023
12
+
13
+ ### Changed
14
+ - Fixed an edge case where `adapter.reset` were failing if the cache is empty
15
+
16
+ ## v10.2.0 3rd April 2023
17
+
18
+ ### Changed
19
+ - Fixed caching of `last_transition` [#505](https://github.com/gocardless/statesman/pull/505)
20
+
21
+ ## v10.1.0 10th March 2023
22
+
23
+ ### CHanged
24
+ - Add the source location of the guard callback to `Statesman::GuardFailedError`
25
+
1
26
  ## v10.0.0 17th May 2022
2
27
 
3
28
  ### Changed
data/Gemfile CHANGED
@@ -11,5 +11,6 @@ elsif ENV['RAILS_VERSION']
11
11
  end
12
12
  group :development do
13
13
  # test/unit is no longer bundled with Ruby 2.2, but required by Rails
14
+ gem "pry"
14
15
  gem "test-unit", "~> 3.3" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.2.0")
15
16
  end
data/README.md CHANGED
@@ -611,6 +611,30 @@ describe "some callback" do
611
611
  end
612
612
  ```
613
613
 
614
+ ## Compatibility with type checkers
615
+
616
+ Including ActiveRecordQueries to your model can cause issues with type checkers
617
+ such as Sorbet, this is because this technically is using a dynamic include,
618
+ which is not supported by Sorbet.
619
+
620
+ To avoid these issues you can instead include the TypeSafeActiveRecordQueries
621
+ module and pass in configuration.
622
+
623
+ ```ruby
624
+ class Order < ActiveRecord::Base
625
+ has_many :order_transitions, autosave: false
626
+
627
+ include Statesman::Adapters::TypeSafeActiveRecordQueries
628
+
629
+ configure_state_machine transition_class: OrderTransition,
630
+ initial_state: :pending
631
+
632
+ def state_machine
633
+ @state_machine ||= OrderStateMachine.new(self, transition_class: OrderTransition)
634
+ end
635
+ end
636
+ ```
637
+
614
638
  # Third-party extensions
615
639
 
616
640
  [statesman-sequel](https://github.com/badosu/statesman-sequel) - An adapter to make Statesman work with [Sequel](https://github.com/jeremyevans/sequel)
@@ -7,7 +7,7 @@ module Statesman
7
7
  class ActiveRecordTransitionGenerator < Rails::Generators::Base
8
8
  include Statesman::GeneratorHelpers
9
9
 
10
- desc "Create an ActiveRecord-based transition model"\
10
+ desc "Create an ActiveRecord-based transition model" \
11
11
  "with the required attributes"
12
12
 
13
13
  argument :parent, type: :string, desc: "Your parent model name"
@@ -11,7 +11,7 @@ module Statesman
11
11
  end
12
12
 
13
13
  def migration_class_name
14
- klass.gsub(/::/, "").pluralize
14
+ klass.gsub("::", "").pluralize
15
15
  end
16
16
 
17
17
  def next_migration_number
@@ -52,7 +52,7 @@ module Statesman
52
52
 
53
53
  raise
54
54
  ensure
55
- @last_transition = nil
55
+ reset
56
56
  end
57
57
 
58
58
  def history(force_reload: false)
@@ -65,23 +65,24 @@ module Statesman
65
65
  end
66
66
  end
67
67
 
68
- # rubocop:disable Naming/MemoizedInstanceVariableName
69
68
  def last(force_reload: false)
70
69
  if force_reload
71
70
  @last_transition = history(force_reload: true).last
71
+ elsif instance_variable_defined?(:@last_transition)
72
+ @last_transition
72
73
  else
73
- @last_transition ||= history.last
74
+ @last_transition = history.last
74
75
  end
75
76
  end
76
- # rubocop:enable Naming/MemoizedInstanceVariableName
77
77
 
78
78
  def reset
79
- @last_transition = nil
79
+ if instance_variable_defined?(:@last_transition)
80
+ remove_instance_variable(:@last_transition)
81
+ end
80
82
  end
81
83
 
82
84
  private
83
85
 
84
- # rubocop:disable Metrics/MethodLength
85
86
  def create_transition(from, to, metadata)
86
87
  transition = transitions_for_parent.build(
87
88
  default_transition_attributes(to, metadata),
@@ -118,7 +119,6 @@ module Statesman
118
119
 
119
120
  transition
120
121
  end
121
- # rubocop:enable Metrics/MethodLength
122
122
 
123
123
  def default_transition_attributes(to, metadata)
124
124
  {
@@ -159,13 +159,24 @@ module Statesman
159
159
 
160
160
  def most_recent_transitions(most_recent_id = nil)
161
161
  if most_recent_id
162
- transitions_of_parent.and(
162
+ concrete_transitions_of_parent.and(
163
163
  transition_table[:id].eq(most_recent_id).or(
164
164
  transition_table[:most_recent].eq(true),
165
165
  ),
166
166
  )
167
167
  else
168
- transitions_of_parent.and(transition_table[:most_recent].eq(true))
168
+ concrete_transitions_of_parent.and(transition_table[:most_recent].eq(true))
169
+ end
170
+ end
171
+
172
+ def concrete_transitions_of_parent
173
+ if transition_sti?
174
+ transitions_of_parent.and(
175
+ transition_table[transition_class.inheritance_column].
176
+ eq(transition_class.name),
177
+ )
178
+ else
179
+ transitions_of_parent
169
180
  end
170
181
  end
171
182
 
@@ -231,7 +242,7 @@ module Statesman
231
242
  end
232
243
 
233
244
  def next_sort_key
234
- (last && last.sort_key + 10) || 10
245
+ (last && (last.sort_key + 10)) || 10
235
246
  end
236
247
 
237
248
  def serialized?(transition_class)
@@ -264,13 +275,18 @@ module Statesman
264
275
  end
265
276
  end
266
277
 
267
- def parent_join_foreign_key
268
- association =
269
- parent_model.class.
270
- reflect_on_all_associations(:has_many).
271
- find { |r| r.name.to_s == @association_name.to_s }
278
+ def transition_sti?
279
+ transition_class.column_names.include?(transition_class.inheritance_column)
280
+ end
272
281
 
273
- association_join_primary_key(association)
282
+ def parent_association
283
+ parent_model.class.
284
+ reflect_on_all_associations(:has_many).
285
+ find { |r| r.name.to_s == @association_name.to_s }
286
+ end
287
+
288
+ def parent_join_foreign_key
289
+ association_join_primary_key(parent_association)
274
290
  end
275
291
 
276
292
  def association_join_primary_key(association)
@@ -49,6 +49,14 @@ module Statesman
49
49
 
50
50
  define_in_state(base, query_builder)
51
51
  define_not_in_state(base, query_builder)
52
+
53
+ define_method(:reload) do |*a|
54
+ instance = super(*a)
55
+ if instance.respond_to?(:state_machine, true)
56
+ instance.send(:state_machine).reset
57
+ end
58
+ instance
59
+ end
52
60
  end
53
61
 
54
62
  private
@@ -95,18 +103,18 @@ module Statesman
95
103
  def states_where(states)
96
104
  if initial_state.to_s.in?(states.map(&:to_s))
97
105
  "#{most_recent_transition_alias}.to_state IN (?) OR " \
98
- "#{most_recent_transition_alias}.to_state IS NULL"
106
+ "#{most_recent_transition_alias}.to_state IS NULL"
99
107
  else
100
108
  "#{most_recent_transition_alias}.to_state IN (?) AND " \
101
- "#{most_recent_transition_alias}.to_state IS NOT NULL"
109
+ "#{most_recent_transition_alias}.to_state IS NOT NULL"
102
110
  end
103
111
  end
104
112
 
105
113
  def most_recent_transition_join
106
114
  "LEFT OUTER JOIN #{model_table} AS #{most_recent_transition_alias} " \
107
- "ON #{model.table_name}.#{model_primary_key} = " \
108
- "#{most_recent_transition_alias}.#{model_foreign_key} " \
109
- "AND #{most_recent_transition_alias}.most_recent = #{db_true}"
115
+ "ON #{model.table_name}.#{model_primary_key} = " \
116
+ "#{most_recent_transition_alias}.#{model_foreign_key} " \
117
+ "AND #{most_recent_transition_alias}.most_recent = #{db_true}"
110
118
  end
111
119
 
112
120
  private
@@ -44,7 +44,7 @@ module Statesman
44
44
  private
45
45
 
46
46
  def next_sort_key
47
- (last && last.sort_key + 10) || 10
47
+ (last && (last.sort_key + 10)) || 10
48
48
  end
49
49
  end
50
50
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statesman
4
+ module Adapters
5
+ module TypeSafeActiveRecordQueries
6
+ def configure_state_machine(args = {})
7
+ transition_class = args.fetch(:transition_class)
8
+ initial_state = args.fetch(:initial_state)
9
+
10
+ include(
11
+ ActiveRecordQueries::ClassMethods.new(
12
+ transition_class: transition_class,
13
+ initial_state: initial_state,
14
+ most_recent_transition_alias: try(:most_recent_transition_alias),
15
+ transition_name: try(:transition_name),
16
+ ),
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
@@ -28,13 +28,15 @@ module Statesman
28
28
  end
29
29
 
30
30
  class GuardFailedError < StandardError
31
- def initialize(from, to)
31
+ def initialize(from, to, callback)
32
32
  @from = from
33
33
  @to = to
34
+ @callback = callback
34
35
  super(_message)
36
+ set_backtrace(callback.source_location.join(":")) if callback&.source_location
35
37
  end
36
38
 
37
- attr_reader :from, :to
39
+ attr_reader :from, :to, :callback
38
40
 
39
41
  private
40
42
 
@@ -52,8 +54,8 @@ module Statesman
52
54
 
53
55
  def _message(transition_class_name)
54
56
  "#{transition_class_name}#metadata is not serialized. If you " \
55
- "are using a non-json column type, you should `include " \
56
- "Statesman::Adapters::ActiveRecordTransition`"
57
+ "are using a non-json column type, you should `include " \
58
+ "Statesman::Adapters::ActiveRecordTransition`"
57
59
  end
58
60
  end
59
61
 
@@ -66,9 +68,9 @@ module Statesman
66
68
 
67
69
  def _message(transition_class_name)
68
70
  "#{transition_class_name}#metadata column type cannot be json " \
69
- "and serialized simultaneously. If you are using a json " \
70
- "column type, it is not necessary to `include " \
71
- "Statesman::Adapters::ActiveRecordTransition`"
71
+ "and serialized simultaneously. If you are using a json " \
72
+ "column type, it is not necessary to `include " \
73
+ "Statesman::Adapters::ActiveRecordTransition`"
72
74
  end
73
75
  end
74
76
  end
@@ -6,7 +6,7 @@ require_relative "exceptions"
6
6
  module Statesman
7
7
  class Guard < Callback
8
8
  def call(*args)
9
- raise GuardFailedError.new(from, to) unless super(*args)
9
+ raise GuardFailedError.new(from, to, callback) unless super(*args)
10
10
  end
11
11
  end
12
12
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Statesman
4
- VERSION = "10.0.0"
4
+ VERSION = "10.2.3"
5
5
  end
data/lib/statesman.rb CHANGED
@@ -14,6 +14,8 @@ module Statesman
14
14
  "statesman/adapters/active_record_transition"
15
15
  autoload :ActiveRecordQueries,
16
16
  "statesman/adapters/active_record_queries"
17
+ autoload :TypeSafeActiveRecordQueries,
18
+ "statesman/adapters/type_safe_active_record_queries"
17
19
  end
18
20
  require "statesman/railtie" if defined?(::Rails::Railtie)
19
21
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  namespace :statesman do
4
- desc "Set most_recent to false for old transitions and to true for the "\
4
+ desc "Set most_recent to false for old transitions and to true for the " \
5
5
  "latest one. Safe to re-run"
6
6
  task :backfill_most_recent, [:parent_model_name] => :environment do |_, args|
7
7
  parent_model_name = args.parent_model_name
@@ -56,8 +56,8 @@ namespace :statesman do
56
56
  end
57
57
 
58
58
  done_models += batch_size
59
- puts "Updated #{transition_class.name.pluralize} for "\
60
- "#{[done_models, total_models].min}/#{total_models} "\
59
+ puts "Updated #{transition_class.name.pluralize} for " \
60
+ "#{[done_models, total_models].min}/#{total_models} " \
61
61
  "#{parent_model_name.pluralize}"
62
62
  end
63
63
  end
data/spec/spec_helper.rb CHANGED
@@ -48,6 +48,8 @@ RSpec.configure do |config|
48
48
  my_namespace_my_active_record_model_transitions
49
49
  other_active_record_models
50
50
  other_active_record_model_transitions
51
+ sti_active_record_models
52
+ sti_active_record_model_transitions
51
53
  ]
52
54
  tables.each do |table_name|
53
55
  sql = "DROP TABLE IF EXISTS #{table_name};"
@@ -72,6 +74,15 @@ RSpec.configure do |config|
72
74
  OtherActiveRecordModelTransition.reset_column_information
73
75
  end
74
76
 
77
+ def prepare_sti_model_table
78
+ CreateStiActiveRecordModelMigration.migrate(:up)
79
+ end
80
+
81
+ def prepare_sti_transitions_table
82
+ CreateStiActiveRecordModelTransitionMigration.migrate(:up)
83
+ StiActiveRecordModelTransition.reset_column_information
84
+ end
85
+
75
86
  MyNamespace::MyActiveRecordModelTransition.serialize(:metadata, JSON)
76
87
  end
77
88
  end
@@ -117,8 +117,8 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
117
117
  subject(:not_in_state) { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
118
118
 
119
119
  it do
120
- expect(not_in_state).to match_array([initial_state_model,
121
- returned_to_initial_model])
120
+ expect(not_in_state).to contain_exactly(initial_state_model,
121
+ returned_to_initial_model)
122
122
  end
123
123
  end
124
124
 
@@ -126,8 +126,8 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
126
126
  subject(:not_in_state) { MyActiveRecordModel.not_in_state(%i[succeeded failed]) }
127
127
 
128
128
  it do
129
- expect(not_in_state).to match_array([initial_state_model,
130
- returned_to_initial_model])
129
+ expect(not_in_state).to contain_exactly(initial_state_model,
130
+ returned_to_initial_model)
131
131
  end
132
132
  end
133
133
  end
@@ -254,7 +254,7 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
254
254
  end
255
255
 
256
256
  it "does not raise an error" do
257
- expect { check_missing_methods! }.to_not raise_exception(NotImplementedError)
257
+ expect { check_missing_methods! }.to_not raise_exception
258
258
  end
259
259
  end
260
260