statesman 9.0.1 → 10.1.0

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: 145d2a6b60abe5fb64e8010676cb63b62ba1869daa4343eb538bdf1d261c885e
4
- data.tar.gz: cbcc8f735327409a89a2b238fefdfb6ef4b9e9595b983f7a648df800ca8c2aed
3
+ metadata.gz: e3ff12f10c3b26c96652704af9008eb0154a0d1686ed5c6d8ddcdb18a04dbadc
4
+ data.tar.gz: 1a5a5744e82f569de82a75f8eb87a251e0ddd5fe22f835ca5de4b2d21fa87ecf
5
5
  SHA512:
6
- metadata.gz: 9eeef7b26150628a370117dccbcde1e40bc1dbd15957fca96f18232e167b5bcca58a42f47a2718a6927bfcb08449b4f7515fa48ef68ed281f79e12fad96d40f2
7
- data.tar.gz: b793c7af514ce22564326d697880179caceadff2bebc6c03b02aa5a523e040ffd17e5dff8475b9fad7ed567f6f0f8849fe97ddb6745a7b3d86cd7aceb71e0264
6
+ metadata.gz: 6762062d6fe240f1acc32e742f5b333dd80e2fb20a5e6419200b7b78b0a29d3d37df843ee445afb317ac4db1c142249a233ed4217adeeb6e4242538d9e1fa096
7
+ data.tar.gz: 6df883f757f7de70a31cfa1758de70f1b44b753b95b23942317f7c4638f107a88d4c7bedc654fbf286611125755fa96e908b7c4158d7b28fac7094c09ec65b18
@@ -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/.gitignore CHANGED
@@ -1,18 +1,71 @@
1
1
  *.gem
2
2
  *.rbc
3
- .bundle
4
- .config
5
- .rspec
6
- .yardoc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ # Ignore Byebug command history file.
17
+ .byebug_history
18
+
19
+ ## Specific to RubyMotion:
20
+ .dat*
21
+ .repl_history
22
+ build/
23
+ *.bridgesupport
24
+ build-iPhoneOS/
25
+ build-iPhoneSimulator/
26
+
27
+ ## Specific to RubyMotion (use of CocoaPods):
28
+ #
29
+ # We recommend against adding the Pods directory to your .gitignore. However
30
+ # you should judge for yourself, the pros and cons are mentioned at:
31
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32
+ #
33
+ # vendor/Pods/
34
+
35
+ ## Documentation cache and generated files:
36
+ /.yardoc/
37
+ /_yardoc/
38
+ /doc/
39
+ /rdoc/
40
+
41
+ ## Environment normalization:
42
+ /.bundle/
43
+ /vendor/bundle
44
+ /lib/bundler/man/
45
+
46
+ # for a library or gem, you might want to ignore these files since the code is
47
+ # intended to run in multiple environments; otherwise, check them in:
7
48
  Gemfile.lock
8
- InstalledFiles
9
- _yardoc
10
- coverage
11
- doc/
12
- lib/bundler/man
13
- pkg
14
- rdoc
15
- spec/reports
16
- test/tmp
17
- test/version_tmp
18
- tmp
49
+ # .ruby-version
50
+ # .ruby-gemset
51
+
52
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53
+ .rvmrc
54
+
55
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
+ # .rubocop-https?--*
57
+
58
+ # Project-specific ignores
59
+ .rspec
60
+
61
+ # VSCode
62
+ .vscode
63
+
64
+ # Local History for Visual Studio Code
65
+ .history/
66
+
67
+ # Built Visual Studio Code Extensions
68
+ *.vsix
69
+
70
+ # JetBrains
71
+ .idea
data/.rubocop.yml CHANGED
@@ -5,3 +5,12 @@ inherit_gem:
5
5
 
6
6
  AllCops:
7
7
  TargetRubyVersion: 3.0
8
+
9
+ Metrics/AbcSize:
10
+ Max: 60
11
+
12
+ Metrics/CyclomaticComplexity:
13
+ Max: 10
14
+
15
+ Metrics/PerceivedComplexity:
16
+ Max: 11
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## v10.1.0 10th March 2023
2
+
3
+ ### CHanged
4
+ - Add the source location of the guard callback to `Statesman::GuardFailedError`
5
+
6
+ ## v10.0.0 17th May 2022
7
+
8
+ ### Changed
9
+ - Added support for Ruby 3.1 [#462](https://github.com/gocardless/statesman/pull/462)
10
+ - Removed support for Ruby 2.5 and 2.6 [#462](https://github.com/gocardless/statesman/pull/462)
11
+ - Added `remove_state` and `remove_transitions` methods to `Statesman::Machine` [#464](https://github.com/gocardless/statesman/pull/464)
12
+
1
13
  ## v9.0.1 4th February 2021
2
14
 
3
15
  ### Changed
data/README.md CHANGED
@@ -30,7 +30,7 @@ protection.
30
30
  To get started, just add Statesman to your `Gemfile`, and then run `bundle`:
31
31
 
32
32
  ```ruby
33
- gem 'statesman', '~> 8.0.3'
33
+ gem 'statesman', '~> 10.0.0'
34
34
  ```
35
35
 
36
36
  ## Usage
@@ -116,6 +116,52 @@ Order.in_state(:cancelled) # => [#<Order id: "123">]
116
116
  Order.not_in_state(:checking_out) # => [#<Order id: "123">]
117
117
  ```
118
118
 
119
+ If you'd like, you can also define a template for a generic state machine, then alter classes which extend it as required:
120
+
121
+ ```ruby
122
+ module Template
123
+ def define_states
124
+ state :a, initial: true
125
+ state :b
126
+ state :c
127
+ end
128
+
129
+ def define_transitions
130
+ transition from: :a, to: :b
131
+ transition from: :b, to: :c
132
+ transition from: :c, to: :a
133
+ end
134
+ end
135
+
136
+ class Circular
137
+ include Statesman::Machine
138
+ extend Template
139
+
140
+ define_states
141
+ define_transitions
142
+ end
143
+
144
+ class Linear
145
+ include Statesman::Machine
146
+ extend Template
147
+
148
+ define_states
149
+ define_transitions
150
+
151
+ remove_transitions from: :c, to: :a
152
+ end
153
+
154
+ class Shorter
155
+ include Statesman::Machine
156
+ extend Template
157
+
158
+ define_states
159
+ define_transitions
160
+
161
+ remove_state :c
162
+ end
163
+ ```
164
+
119
165
  ## Persistence
120
166
 
121
167
  By default Statesman stores transition history in memory only. It can be
@@ -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"
@@ -81,7 +81,6 @@ module Statesman
81
81
 
82
82
  private
83
83
 
84
- # rubocop:disable Metrics/MethodLength
85
84
  def create_transition(from, to, metadata)
86
85
  transition = transitions_for_parent.build(
87
86
  default_transition_attributes(to, metadata),
@@ -118,7 +117,6 @@ module Statesman
118
117
 
119
118
  transition
120
119
  end
121
- # rubocop:enable Metrics/MethodLength
122
120
 
123
121
  def default_transition_attributes(to, metadata)
124
122
  {
@@ -231,7 +229,7 @@ module Statesman
231
229
  end
232
230
 
233
231
  def next_sort_key
234
- (last && last.sort_key + 10) || 10
232
+ (last && (last.sort_key + 10)) || 10
235
233
  end
236
234
 
237
235
  def serialized?(transition_class)
@@ -95,18 +95,18 @@ module Statesman
95
95
  def states_where(states)
96
96
  if initial_state.to_s.in?(states.map(&:to_s))
97
97
  "#{most_recent_transition_alias}.to_state IN (?) OR " \
98
- "#{most_recent_transition_alias}.to_state IS NULL"
98
+ "#{most_recent_transition_alias}.to_state IS NULL"
99
99
  else
100
100
  "#{most_recent_transition_alias}.to_state IN (?) AND " \
101
- "#{most_recent_transition_alias}.to_state IS NOT NULL"
101
+ "#{most_recent_transition_alias}.to_state IS NOT NULL"
102
102
  end
103
103
  end
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}.#{model_primary_key} = " \
108
- "#{most_recent_transition_alias}.#{model_foreign_key} " \
109
- "AND #{most_recent_transition_alias}.most_recent = #{db_true}"
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}"
110
110
  end
111
111
 
112
112
  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
@@ -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
@@ -42,6 +42,17 @@ module Statesman
42
42
  states << name
43
43
  end
44
44
 
45
+ def remove_state(state_name)
46
+ state_name = state_name.to_s
47
+
48
+ remove_transitions(from: state_name)
49
+ remove_transitions(to: state_name)
50
+ remove_callbacks(from: state_name)
51
+ remove_callbacks(to: state_name)
52
+
53
+ @states.delete(state_name.to_s)
54
+ end
55
+
45
56
  def successors
46
57
  @successors ||= {}
47
58
  end
@@ -70,6 +81,20 @@ module Statesman
70
81
  successors[from] += to
71
82
  end
72
83
 
84
+ def remove_transitions(from: nil, to: nil)
85
+ raise ArgumentError, "Both from and to can't be nil!" if from.nil? && to.nil?
86
+ return if successors.nil?
87
+
88
+ if from.present?
89
+ @successors[from.to_s].delete(to.to_s) if to.present?
90
+ @successors.delete(from.to_s) if to.nil? || successors[from.to_s].empty?
91
+ elsif to.present?
92
+ @successors.
93
+ transform_values! { |to_states| to_states - [to.to_s] }.
94
+ filter! { |_from_state, to_states| to_states.any? }
95
+ end
96
+ end
97
+
73
98
  def before_transition(options = {}, &block)
74
99
  add_callback(callback_type: :before, callback_class: Callback,
75
100
  from: options[:from], to: options[:to], &block)
@@ -151,6 +176,33 @@ module Statesman
151
176
  callback_class.new(from: from, to: to, callback: block)
152
177
  end
153
178
 
179
+ def remove_callbacks(from: nil, to: nil)
180
+ raise ArgumentError, "Both from and to can't be nil!" if from.nil? && to.nil?
181
+ return if callbacks.nil?
182
+
183
+ @callbacks.transform_values! do |callbacks|
184
+ filter_callbacks(callbacks, from: from, to: to)
185
+ end
186
+ end
187
+
188
+ def filter_callbacks(callbacks, from: nil, to: nil)
189
+ callbacks.filter_map do |callback|
190
+ next if callback.from == from && to.nil?
191
+
192
+ if callback.to.include?(to) && (from.nil? || callback.from == from)
193
+ next if callback.to == [to]
194
+
195
+ callback = Statesman::Callback.new({
196
+ from: callback.from,
197
+ to: callback.to - [to],
198
+ callback: callback.callback,
199
+ })
200
+ end
201
+
202
+ callback
203
+ end
204
+ end
205
+
154
206
  def validate_callback_type_and_class(callback_type, callback_class)
155
207
  raise ArgumentError, "missing keyword: callback_type" if callback_type.nil?
156
208
  raise ArgumentError, "missing keyword: callback_class" if callback_class.nil?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Statesman
4
- VERSION = "9.0.1"
4
+ VERSION = "10.1.0"
5
5
  end
@@ -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
@@ -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
@@ -64,12 +64,18 @@ describe Statesman do
64
64
  end
65
65
 
66
66
  describe "GuardFailedError" do
67
- subject(:error) { Statesman::GuardFailedError.new("from", "to") }
67
+ subject(:error) { Statesman::GuardFailedError.new("from", "to", callback) }
68
+
69
+ let(:callback) { -> { "hello" } }
68
70
 
69
71
  its(:message) do
70
72
  is_expected.to eq("Guard on transition from: 'from' to 'to' returned false")
71
73
  end
72
74
 
75
+ its(:backtrace) do
76
+ is_expected.to eq([callback.source_location.join(":")])
77
+ end
78
+
73
79
  its "string matches its message" do
74
80
  expect(error.to_s).to eq(error.message)
75
81
  end
@@ -27,6 +27,112 @@ describe Statesman::Machine do
27
27
  end
28
28
  end
29
29
 
30
+ describe ".remove_state" do
31
+ subject(:remove_state) { -> { machine.remove_state(:x) } }
32
+
33
+ before do
34
+ machine.class_eval do
35
+ state :x
36
+ state :y
37
+ state :z
38
+ end
39
+ end
40
+
41
+ it "removes the state" do
42
+ expect(remove_state).
43
+ to change(machine, :states).
44
+ from(match_array(%w[x y z])).
45
+ to(%w[y z])
46
+ end
47
+
48
+ context "with a transition from the removed state" do
49
+ before { machine.transition from: :x, to: :y }
50
+
51
+ it "removes the transition" do
52
+ expect(remove_state).
53
+ to change(machine, :successors).
54
+ from({ "x" => ["y"] }).
55
+ to({})
56
+ end
57
+
58
+ context "with multiple transitions" do
59
+ before { machine.transition from: :x, to: :z }
60
+
61
+ it "removes all transitions" do
62
+ expect(remove_state).
63
+ to change(machine, :successors).
64
+ from({ "x" => %w[y z] }).
65
+ to({})
66
+ end
67
+ end
68
+ end
69
+
70
+ context "with a transition to the removed state" do
71
+ before { machine.transition from: :y, to: :x }
72
+
73
+ it "removes the transition" do
74
+ expect(remove_state).
75
+ to change(machine, :successors).
76
+ from({ "y" => ["x"] }).
77
+ to({})
78
+ end
79
+
80
+ context "with multiple transitions" do
81
+ before { machine.transition from: :z, to: :x }
82
+
83
+ it "removes all transitions" do
84
+ expect(remove_state).
85
+ to change(machine, :successors).
86
+ from({ "y" => ["x"], "z" => ["x"] }).
87
+ to({})
88
+ end
89
+ end
90
+ end
91
+
92
+ context "with a callback from the removed state" do
93
+ before do
94
+ machine.class_eval do
95
+ transition from: :x, to: :y
96
+ transition from: :x, to: :z
97
+ guard_transition(from: :x) { return false }
98
+ guard_transition(from: :x, to: :z) { return true }
99
+ end
100
+ end
101
+
102
+ let(:guards) do
103
+ [having_attributes(from: "x", to: []), having_attributes(from: "x", to: ["z"])]
104
+ end
105
+
106
+ it "removes the guard" do
107
+ expect(remove_state).
108
+ to change(machine, :callbacks).
109
+ from(a_hash_including(guards: match_array(guards))).
110
+ to(a_hash_including(guards: []))
111
+ end
112
+ end
113
+
114
+ context "with a callback to the removed state" do
115
+ before do
116
+ machine.class_eval do
117
+ transition from: :y, to: :x
118
+ guard_transition(to: :x) { return false }
119
+ guard_transition(from: :y, to: :x) { return true }
120
+ end
121
+ end
122
+
123
+ let(:guards) do
124
+ [having_attributes(from: nil, to: ["x"]), having_attributes(from: "y", to: ["x"])]
125
+ end
126
+
127
+ it "removes the guard" do
128
+ expect(remove_state).
129
+ to change(machine, :callbacks).
130
+ from(a_hash_including(guards: match_array(guards))).
131
+ to(a_hash_including(guards: []))
132
+ end
133
+ end
134
+ end
135
+
30
136
  describe ".retry_conflicts" do
31
137
  subject(:transition_state) do
32
138
  described_class.retry_conflicts(retry_attempts) do
@@ -170,6 +276,42 @@ describe Statesman::Machine do
170
276
  end
171
277
  end
172
278
 
279
+ describe ".remove_transitions" do
280
+ before do
281
+ machine.class_eval do
282
+ state :x
283
+ state :y
284
+ state :z
285
+ transition from: :x, to: :y
286
+ transition from: :x, to: :z
287
+ transition from: :y, to: :z
288
+ end
289
+ end
290
+
291
+ let(:initial_successors) { { "x" => %w[y z], "y" => ["z"] } }
292
+
293
+ it "removes the correct transitions when given a from state" do
294
+ expect { machine.remove_transitions(from: :x) }.
295
+ to change(machine, :successors).
296
+ from(initial_successors).
297
+ to({ "y" => ["z"] })
298
+ end
299
+
300
+ it "removes the correct transitions when given a to state" do
301
+ expect { machine.remove_transitions(to: :z) }.
302
+ to change(machine, :successors).
303
+ from(initial_successors).
304
+ to({ "x" => ["y"] })
305
+ end
306
+
307
+ it "removes the correct transitions when given a from and to state" do
308
+ expect { machine.remove_transitions(from: :x, to: :z) }.
309
+ to change(machine, :successors).
310
+ from(initial_successors).
311
+ to({ "x" => ["y"], "y" => ["z"] })
312
+ end
313
+ end
314
+
173
315
  describe ".validate_callback_condition" do
174
316
  before do
175
317
  machine.class_eval do
@@ -793,10 +935,10 @@ describe Statesman::Machine do
793
935
  it { is_expected.to be(:some_state) }
794
936
  end
795
937
 
796
- context "when it is unsuccesful" do
938
+ context "when it is unsuccessful" do
797
939
  before do
798
940
  allow(instance).to receive(:transition_to!).
799
- and_raise(Statesman::GuardFailedError.new(:x, :some_state))
941
+ and_raise(Statesman::GuardFailedError.new(:x, :some_state, nil))
800
942
  end
801
943
 
802
944
  it { is_expected.to be_falsey }
@@ -64,7 +64,6 @@ class CreateMyActiveRecordModelMigration < MIGRATION_CLASS
64
64
  end
65
65
 
66
66
  # TODO: make this a module we can extend from the app? Or a generator?
67
- # rubocop:disable Metrics/MethodLength
68
67
  class CreateMyActiveRecordModelTransitionMigration < MIGRATION_CLASS
69
68
  def change
70
69
  create_table :my_active_record_model_transitions do |t|
@@ -110,7 +109,6 @@ class CreateMyActiveRecordModelTransitionMigration < MIGRATION_CLASS
110
109
  end
111
110
  end
112
111
  end
113
- # rubocop:enable Metrics/MethodLength
114
112
 
115
113
  class OtherActiveRecordModel < ActiveRecord::Base
116
114
  has_many :other_active_record_model_transitions, autosave: false
@@ -144,7 +142,6 @@ class CreateOtherActiveRecordModelMigration < MIGRATION_CLASS
144
142
  end
145
143
  end
146
144
 
147
- # rubocop:disable Metrics/MethodLength
148
145
  class CreateOtherActiveRecordModelTransitionMigration < MIGRATION_CLASS
149
146
  def change
150
147
  create_table :other_active_record_model_transitions do |t|
@@ -177,18 +174,17 @@ class CreateOtherActiveRecordModelTransitionMigration < MIGRATION_CLASS
177
174
  %i[other_active_record_model_id most_recent],
178
175
  unique: true,
179
176
  where: "most_recent",
180
- name: "index_other_active_record_model_transitions_"\
177
+ name: "index_other_active_record_model_transitions_" \
181
178
  "parent_latest"
182
179
  else
183
180
  add_index :other_active_record_model_transitions,
184
181
  %i[other_active_record_model_id most_recent],
185
182
  unique: true,
186
- name: "index_other_active_record_model_transitions_"\
183
+ name: "index_other_active_record_model_transitions_" \
187
184
  "parent_latest"
188
185
  end
189
186
  end
190
187
  end
191
- # rubocop:enable Metrics/MethodLength
192
188
 
193
189
  class DropMostRecentColumn < MIGRATION_CLASS
194
190
  def change
@@ -242,7 +238,6 @@ class CreateNamespacedARModelMigration < MIGRATION_CLASS
242
238
  end
243
239
  end
244
240
 
245
- # rubocop:disable Metrics/MethodLength
246
241
  class CreateNamespacedARModelTransitionMigration < MIGRATION_CLASS
247
242
  def change
248
243
  create_table :my_namespace_my_active_record_model_transitions do |t|
@@ -282,5 +277,4 @@ class CreateNamespacedARModelTransitionMigration < MIGRATION_CLASS
282
277
  name: "index_namespace_model_transitions_parent_latest"
283
278
  end
284
279
  end
285
- # rubocop:enable Metrics/MethodLength
286
280
  end
data/statesman.gemspec CHANGED
@@ -18,24 +18,23 @@ Gem::Specification.new do |spec|
18
18
 
19
19
  spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
20
20
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
21
  spec.require_paths = ["lib"]
23
22
 
24
- spec.required_ruby_version = ">= 2.5"
23
+ spec.required_ruby_version = ">= 2.7"
25
24
 
26
25
  spec.add_development_dependency "ammeter", "~> 1.1"
27
26
  spec.add_development_dependency "bundler", "~> 2"
28
- spec.add_development_dependency "gc_ruboconfig", "~> 2.26.0"
27
+ spec.add_development_dependency "gc_ruboconfig", "~> 3.6.0"
29
28
  spec.add_development_dependency "mysql2", ">= 0.4", "< 0.6"
30
29
  spec.add_development_dependency "pg", ">= 0.18", "<= 1.3"
31
30
  spec.add_development_dependency "pry"
32
31
  spec.add_development_dependency "rails", ">= 5.2"
33
32
  spec.add_development_dependency "rake", "~> 13.0.0"
34
33
  spec.add_development_dependency "rspec", "~> 3.1"
34
+ spec.add_development_dependency "rspec-github", "~> 2.3.1"
35
35
  spec.add_development_dependency "rspec-its", "~> 1.1"
36
- spec.add_development_dependency "rspec_junit_formatter", "~> 0.4.0"
37
36
  spec.add_development_dependency "rspec-rails", "~> 3.1"
38
- spec.add_development_dependency "sqlite3", "~> 1.4.2"
37
+ spec.add_development_dependency "sqlite3", "~> 1.6.1"
39
38
  spec.add_development_dependency "timecop", "~> 0.9.1"
40
39
 
41
40
  spec.metadata = {
@@ -44,5 +43,6 @@ Gem::Specification.new do |spec|
44
43
  "documentation_uri" => "#{GITHUB_URL}/blob/master/README.md",
45
44
  "homepage_uri" => GITHUB_URL,
46
45
  "source_code_uri" => GITHUB_URL,
46
+ "rubygems_mfa_required" => "true",
47
47
  }
48
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: 9.0.1
4
+ version: 10.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: 2022-02-04 00:00:00.000000000 Z
11
+ date: 2023-03-10 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: 2.26.0
47
+ version: 3.6.0
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: 2.26.0
54
+ version: 3.6.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: mysql2
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -149,33 +149,33 @@ dependencies:
149
149
  - !ruby/object:Gem::Version
150
150
  version: '3.1'
151
151
  - !ruby/object:Gem::Dependency
152
- name: rspec-its
152
+ name: rspec-github
153
153
  requirement: !ruby/object:Gem::Requirement
154
154
  requirements:
155
155
  - - "~>"
156
156
  - !ruby/object:Gem::Version
157
- version: '1.1'
157
+ version: 2.3.1
158
158
  type: :development
159
159
  prerelease: false
160
160
  version_requirements: !ruby/object:Gem::Requirement
161
161
  requirements:
162
162
  - - "~>"
163
163
  - !ruby/object:Gem::Version
164
- version: '1.1'
164
+ version: 2.3.1
165
165
  - !ruby/object:Gem::Dependency
166
- name: rspec_junit_formatter
166
+ name: rspec-its
167
167
  requirement: !ruby/object:Gem::Requirement
168
168
  requirements:
169
169
  - - "~>"
170
170
  - !ruby/object:Gem::Version
171
- version: 0.4.0
171
+ version: '1.1'
172
172
  type: :development
173
173
  prerelease: false
174
174
  version_requirements: !ruby/object:Gem::Requirement
175
175
  requirements:
176
176
  - - "~>"
177
177
  - !ruby/object:Gem::Version
178
- version: 0.4.0
178
+ version: '1.1'
179
179
  - !ruby/object:Gem::Dependency
180
180
  name: rspec-rails
181
181
  requirement: !ruby/object:Gem::Requirement
@@ -196,14 +196,14 @@ dependencies:
196
196
  requirements:
197
197
  - - "~>"
198
198
  - !ruby/object:Gem::Version
199
- version: 1.4.2
199
+ version: 1.6.1
200
200
  type: :development
201
201
  prerelease: false
202
202
  version_requirements: !ruby/object:Gem::Requirement
203
203
  requirements:
204
204
  - - "~>"
205
205
  - !ruby/object:Gem::Version
206
- version: 1.4.2
206
+ version: 1.6.1
207
207
  - !ruby/object:Gem::Dependency
208
208
  name: timecop
209
209
  requirement: !ruby/object:Gem::Requirement
@@ -225,8 +225,8 @@ executables: []
225
225
  extensions: []
226
226
  extra_rdoc_files: []
227
227
  files:
228
- - ".circleci/config.yml"
229
228
  - ".github/dependabot.yml"
229
+ - ".github/workflows/tests.yml"
230
230
  - ".gitignore"
231
231
  - ".rubocop.yml"
232
232
  - ".rubocop_todo.yml"
@@ -290,6 +290,7 @@ metadata:
290
290
  documentation_uri: https://github.com/gocardless/statesman/blob/master/README.md
291
291
  homepage_uri: https://github.com/gocardless/statesman
292
292
  source_code_uri: https://github.com/gocardless/statesman
293
+ rubygems_mfa_required: 'true'
293
294
  post_install_message:
294
295
  rdoc_options: []
295
296
  require_paths:
@@ -298,7 +299,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
298
299
  requirements:
299
300
  - - ">="
300
301
  - !ruby/object:Gem::Version
301
- version: '2.5'
302
+ version: '2.7'
302
303
  required_rubygems_version: !ruby/object:Gem::Requirement
303
304
  requirements:
304
305
  - - ">="
@@ -309,24 +310,4 @@ rubygems_version: 3.2.22
309
310
  signing_key:
310
311
  specification_version: 4
311
312
  summary: A statesman-like state machine library
312
- test_files:
313
- - spec/fixtures/add_constraints_to_most_recent_for_bacon_transitions_with_partial_index.rb
314
- - spec/fixtures/add_constraints_to_most_recent_for_bacon_transitions_without_partial_index.rb
315
- - spec/fixtures/add_most_recent_to_bacon_transitions.rb
316
- - spec/generators/statesman/active_record_transition_generator_spec.rb
317
- - spec/generators/statesman/migration_generator_spec.rb
318
- - spec/spec_helper.rb
319
- - spec/statesman/adapters/active_record_queries_spec.rb
320
- - spec/statesman/adapters/active_record_spec.rb
321
- - spec/statesman/adapters/active_record_transition_spec.rb
322
- - spec/statesman/adapters/memory_spec.rb
323
- - spec/statesman/adapters/memory_transition_spec.rb
324
- - spec/statesman/adapters/shared_examples.rb
325
- - spec/statesman/callback_spec.rb
326
- - spec/statesman/config_spec.rb
327
- - spec/statesman/exceptions_spec.rb
328
- - spec/statesman/guard_spec.rb
329
- - spec/statesman/machine_spec.rb
330
- - spec/statesman/utils_spec.rb
331
- - spec/support/active_record.rb
332
- - spec/support/generators_shared_examples.rb
313
+ test_files: []
data/.circleci/config.yml DELETED
@@ -1,127 +0,0 @@
1
- ---
2
- version: 2.1
3
-
4
- references:
5
- bundle_install: &bundle_install
6
- run:
7
- name: Bundle
8
- command: |
9
- gem install bundler --no-document && \
10
- bundle config set no-cache 'true' && \
11
- bundle config set jobs '4' && \
12
- bundle config set retry '3' && \
13
- bundle install
14
-
15
- cache_bundle: &cache_bundle
16
- save_cache:
17
- key: bundle-<< parameters.ruby_version >>-<< parameters.rails_version >>-{{ checksum "statesman.gemspec" }}-{{ checksum "Gemfile" }}
18
- paths:
19
- - vendor/bundle
20
-
21
- restore_bundle: &restore_bundle
22
- restore_cache:
23
- key: bundle-<< parameters.ruby_version >>-<< parameters.rails_version >>-{{ checksum "statesman.gemspec" }}-{{ checksum "Gemfile" }}
24
-
25
- steps: &steps
26
- - add_ssh_keys
27
- - checkout
28
- - run:
29
- name: "Add dependencies"
30
- command: |
31
- sudo apt-get update && sudo apt-get install -y sqlite3 libsqlite3-dev
32
- - *restore_bundle
33
- - *bundle_install
34
- - *cache_bundle
35
- - run: dockerize -wait tcp://localhost:$DATABASE_DEPENDENCY_PORT -timeout 1m
36
- - run:
37
- name: Run specs
38
- command: |
39
- bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile --format progress --format RspecJunitFormatter -o /tmp/circle_artifacts/rspec.xml
40
- - run:
41
- name: "Rubocop"
42
- command: bundle exec rubocop --extra-details --display-style-guide --parallel --force-exclusion
43
- - store_artifacts:
44
- path: /tmp/circle_artifacts/
45
- - store_test_results:
46
- path: /tmp/circle_artifacts/
47
-
48
- ruby_versions: &ruby_versions
49
- - "2.5"
50
- - "2.6"
51
- - "2.7"
52
- - "3.0"
53
-
54
- rails_versions: &rails_versions
55
- - "5.2.6"
56
- - "6.0.4"
57
- - "6.1.4"
58
- - "main"
59
-
60
- mysql_versions: &mysql_versions
61
- - "5.7"
62
-
63
- psql_versions: &psql_versions
64
- - "9.6"
65
-
66
- jobs:
67
- rspec_mysql:
68
- working_directory: /mnt/ramdisk
69
- parameters:
70
- ruby_version:
71
- type: string
72
- rails_version:
73
- type: string
74
- mysql_version:
75
- type: string
76
- docker:
77
- - image: cimg/ruby:<< parameters.ruby_version >>
78
- environment:
79
- CIRCLE_TEST_REPORTS: /tmp/circle_artifacts/
80
- DATABASE_URL: mysql2://foobar:password@127.0.0.1/statesman_test
81
- DATABASE_DEPENDENCY_PORT: "3306"
82
- - image: cimg/mysql:<< parameters.mysql_version >>
83
- environment:
84
- MYSQL_ROOT_PASSWORD: password
85
- MYSQL_USER: foobar
86
- MYSQL_PASSWORD: password
87
- MYSQL_DATABASE: statesman_test
88
- steps: *steps
89
-
90
- rspec_postgres:
91
- working_directory: /mnt/ramdisk
92
- parameters:
93
- ruby_version:
94
- type: string
95
- rails_version:
96
- type: string
97
- psql_version:
98
- type: string
99
- docker:
100
- - image: cimg/ruby:<< parameters.ruby_version >>
101
- environment:
102
- CIRCLE_TEST_REPORTS: /tmp/circle_artifacts/
103
- DATABASE_URL: postgres://postgres@localhost/statesman_test
104
- DATABASE_DEPENDENCY_PORT: "5432"
105
- - image: circleci/postgres:<< parameters.psql_version >>
106
- environment:
107
- POSTGRES_USER: postgres
108
- POSTGRES_DB: statesman_test
109
- POSTGRES_PASSWORD: statesman
110
- steps: *steps
111
-
112
- workflows:
113
- version: 2
114
- tests:
115
- jobs:
116
- - rspec_mysql:
117
- matrix:
118
- parameters:
119
- mysql_version: *mysql_versions
120
- ruby_version: *ruby_versions
121
- rails_version: *rails_versions
122
- - rspec_postgres:
123
- matrix:
124
- parameters:
125
- psql_version: *psql_versions
126
- ruby_version: *ruby_versions
127
- rails_version: *rails_versions