statesman 7.0.1 → 7.4.1

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: 4e098c0148958526a38807f1019f76735c627dcd57124bd61108c52ec5d468a1
4
- data.tar.gz: 7ee21582573f6aa3887b5081e96850080ad450afeec1431365fcab492ef8fc1a
3
+ metadata.gz: 9dc585aeecc837bf65850c562c9f41524959bd07c5f7f889284f534c260911c3
4
+ data.tar.gz: 807ae19092dcce330131916b825964e5f62ff13f8c4bde2e2140dee9d1f9983d
5
5
  SHA512:
6
- metadata.gz: bcc7845c4fe7e31640bfa956f7aae786fd3bbbc395af0a5266e1b18c67cd309265e9b6076cd6ba75c90802f48a1e95c716cd39f5303e7410063b403cc0eec3d1
7
- data.tar.gz: b359e526717980326362978645a5abcd2c1fd8eeaa07ddb6ea41212100c05d4588e752791bf9592495abf6e30d06cb7ea488825266caf3440f66dbd4d22bc903
6
+ metadata.gz: 2688beb079f3b1993bd19b4460834a18a4ca1d5eac9733003c7c5f08c1fd2deb19aeb46103a8f799e89c346ac0704368119ff180ed907928c74587b6a6623ea0
7
+ data.tar.gz: c58db63a374f2474ccc73f0064d9dc089d8e52967b047ef77333fdae9b57eb1798cf428a0e6ec162788eaec99e5758d1bd26118c43944f5edda90261dbcb6845
@@ -56,6 +56,7 @@ jobs:
56
56
  environment:
57
57
  - POSTGRES_USER=postgres
58
58
  - POSTGRES_DB=statesman_test
59
+ - POSTGRES_PASSWORD=statesman
59
60
  steps: *steps
60
61
 
61
62
  build-ruby265-rails-602-mysql:
@@ -83,6 +84,7 @@ jobs:
83
84
  environment:
84
85
  - POSTGRES_USER=postgres
85
86
  - POSTGRES_DB=statesman_test
87
+ - POSTGRES_PASSWORD=statesman
86
88
  steps: *steps
87
89
  build-ruby265-rails-master-mysql:
88
90
  docker:
@@ -110,6 +112,7 @@ jobs:
110
112
  environment:
111
113
  - POSTGRES_USER=postgres
112
114
  - POSTGRES_DB=statesman_test
115
+ - POSTGRES_PASSWORD=statesman
113
116
  steps: *steps
114
117
 
115
118
  build-ruby270-rails-602-mysql:
@@ -137,6 +140,7 @@ jobs:
137
140
  environment:
138
141
  - POSTGRES_USER=postgres
139
142
  - POSTGRES_DB=statesman_test
143
+ - POSTGRES_PASSWORD=statesman
140
144
  steps: *steps
141
145
  build-ruby270-rails-master-mysql:
142
146
  docker:
@@ -164,6 +168,7 @@ jobs:
164
168
  environment:
165
169
  - POSTGRES_USER=postgres
166
170
  - POSTGRES_DB=statesman_test
171
+ - POSTGRES_PASSWORD=statesman
167
172
  steps: *steps
168
173
 
169
174
  workflows:
@@ -1,3 +1,30 @@
1
+ ## v7.4.0 26th August 2020
2
+
3
+ ### Added
4
+
5
+ - [Gem Metadata](https://guides.rubygems.org/specification-reference/#metadata)
6
+ to make finding changes between releases even easier.
7
+
8
+ ## v7.3.0, 24th August 2020
9
+
10
+ ### Changed
11
+
12
+ - Use correct Arel for null [#409](https://github.com/gocardless/statesman/pull/#409)
13
+
14
+ ## v7.2.0, 19th May 2020
15
+
16
+ ### Changed
17
+
18
+ - Set non-empty password for postgres tests [#398](https://github.com/gocardless/statesman/pull/#398)
19
+ - Handle transitions differently for MySQL [#399](https://github.com/gocardless/statesman/pull/#399)
20
+ - pg requirement from >= 0.18, <= 1.1 to >= 0.18, <= 1.3 [#400](https://github.com/gocardless/statesman/pull/#400)
21
+ - Lazily enable mysql gaplock protection [#402](https://github.com/gocardless/statesman/pull/#402)
22
+
23
+ ## v7.1.0, 10th Feb 2020
24
+
25
+ - Fix `to_s` on `TransitionFailedError` & `GuardFailedError`. `.message` and
26
+ `.to_s` diverged when `from` and `to` accessors where added in v4.1.3
27
+
1
28
  ## v7.0.1, 8th Jan 2020
2
29
 
3
30
  - Fix deprecation warning with Ruby 2.7 [#386](https://github.com/gocardless/statesman/pull/386)
data/Gemfile CHANGED
@@ -14,5 +14,5 @@ end
14
14
 
15
15
  group :development do
16
16
  # test/unit is no longer bundled with Ruby 2.2, but required by Rails
17
- gem "test-unit", "~> 3.0" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.2.0")
17
+ gem "test-unit", "~> 3.3" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.2.0")
18
18
  end
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', '~> 7.0.1'
33
+ gem 'statesman', '~> 7.1.0'
34
34
  ```
35
35
 
36
36
  ## Usage
@@ -20,14 +20,25 @@ module Statesman
20
20
  # Example:
21
21
  # Statesman.configure do
22
22
  # storage_adapter Statesman::ActiveRecordAdapter
23
+ # enable_mysql_gaplock_protection
23
24
  # end
24
25
  #
25
26
  def self.configure(&block)
26
- config = Config.new(block)
27
+ @config = Config.new(block)
27
28
  @storage_adapter = config.adapter_class
28
29
  end
29
30
 
30
31
  def self.storage_adapter
31
32
  @storage_adapter || Adapters::Memory
32
33
  end
34
+
35
+ def self.mysql_gaplock_protection?
36
+ return @mysql_gaplock_protection unless @mysql_gaplock_protection.nil?
37
+
38
+ @mysql_gaplock_protection = config.mysql_gaplock_protection?
39
+ end
40
+
41
+ def self.config
42
+ @config ||= Config.new
43
+ end
33
44
  end
@@ -5,9 +5,6 @@ require_relative "../exceptions"
5
5
  module Statesman
6
6
  module Adapters
7
7
  class ActiveRecord
8
- attr_reader :transition_class
9
- attr_reader :parent_model
10
-
11
8
  JSON_COLUMN_TYPES = %w[json jsonb].freeze
12
9
 
13
10
  def self.database_supports_partial_indexes?
@@ -19,6 +16,10 @@ module Statesman
19
16
  end
20
17
  end
21
18
 
19
+ def self.adapter_name
20
+ ::ActiveRecord::Base.connection.adapter_name.downcase
21
+ end
22
+
22
23
  def initialize(transition_class, parent_model, observer, options = {})
23
24
  serialized = serialized?(transition_class)
24
25
  column_type = transition_class.columns_hash["metadata"].sql_type
@@ -29,12 +30,15 @@ module Statesman
29
30
  end
30
31
 
31
32
  @transition_class = transition_class
33
+ @transition_table = transition_class.arel_table
32
34
  @parent_model = parent_model
33
35
  @observer = observer
34
36
  @association_name =
35
37
  options[:association_name] || @transition_class.table_name
36
38
  end
37
39
 
40
+ attr_reader :transition_class, :transition_table, :parent_model
41
+
38
42
  def create(from, to, metadata = {})
39
43
  create_transition(from.to_s, to.to_s, metadata)
40
44
  rescue ::ActiveRecord::RecordNotUnique => e
@@ -63,21 +67,42 @@ module Statesman
63
67
  end
64
68
  end
65
69
 
70
+ def reset
71
+ @last_transition = nil
72
+ end
73
+
66
74
  private
67
75
 
76
+ # rubocop:disable Metrics/MethodLength
68
77
  def create_transition(from, to, metadata)
69
- transition_attributes = { to_state: to,
70
- sort_key: next_sort_key,
71
- metadata: metadata }
72
-
73
- transition_attributes[:most_recent] = true
74
-
75
- transition = transitions_for_parent.build(transition_attributes)
78
+ transition = transitions_for_parent.build(
79
+ default_transition_attributes(to, metadata),
80
+ )
76
81
 
77
82
  ::ActiveRecord::Base.transaction(requires_new: true) do
78
83
  @observer.execute(:before, from, to, transition)
79
- unset_old_most_recent
80
- transition.save!
84
+
85
+ if mysql_gaplock_protection?
86
+ # We save the transition first with most_recent falsy, then mark most_recent
87
+ # true after to avoid letting MySQL acquire a next-key lock which can cause
88
+ # deadlocks.
89
+ #
90
+ # To avoid an additional query, we manually adjust the most_recent attribute
91
+ # on our transition assuming that update_most_recents will have set it to true
92
+
93
+ transition.save!
94
+
95
+ unless update_most_recents(transition.id).positive?
96
+ raise ActiveRecord::Rollback, "failed to update most_recent"
97
+ end
98
+
99
+ transition.assign_attributes(most_recent: true)
100
+ else
101
+ update_most_recents
102
+ transition.assign_attributes(most_recent: true)
103
+ transition.save!
104
+ end
105
+
81
106
  @last_transition = transition
82
107
  @observer.execute(:after, from, to, transition)
83
108
  add_after_commit_callback(from, to, transition)
@@ -85,6 +110,16 @@ module Statesman
85
110
 
86
111
  transition
87
112
  end
113
+ # rubocop:enable Metrics/MethodLength
114
+
115
+ def default_transition_attributes(to, metadata)
116
+ {
117
+ to_state: to,
118
+ sort_key: next_sort_key,
119
+ metadata: metadata,
120
+ most_recent: not_most_recent_value(db_cast: false),
121
+ }
122
+ end
88
123
 
89
124
  def add_after_commit_callback(from, to, transition)
90
125
  ::ActiveRecord::Base.connection.add_transaction_record(
@@ -98,20 +133,92 @@ module Statesman
98
133
  parent_model.send(@association_name)
99
134
  end
100
135
 
101
- def unset_old_most_recent
102
- most_recent = transitions_for_parent.where(most_recent: true)
136
+ # Sets the given transition most_recent = t while unsetting the most_recent of any
137
+ # previous transitions.
138
+ def update_most_recents(most_recent_id = nil)
139
+ update = build_arel_manager(::Arel::UpdateManager)
140
+ update.table(transition_table)
141
+ update.where(most_recent_transitions(most_recent_id))
142
+ update.set(build_most_recents_update_all_values(most_recent_id))
103
143
 
104
- # Check whether the `most_recent` column allows null values. If it
105
- # doesn't, set old records to `false`, otherwise, set them to `NULL`.
106
- #
107
- # Some conditioning here is required to support databases that don't
108
- # support partial indexes. By doing the conditioning on the column,
109
- # rather than Rails' opinion of whether the database supports partial
110
- # indexes, we're robust to DBs later adding support for partial indexes.
111
- if transition_class.columns_hash["most_recent"].null == false
112
- most_recent.update_all(with_updated_timestamp(most_recent: false))
144
+ # MySQL will validate index constraints across the intermediate result of an
145
+ # update. This means we must order our update to deactivate the previous
146
+ # most_recent before setting the new row to be true.
147
+ update.order(transition_table[:most_recent].desc) if mysql_gaplock_protection?
148
+
149
+ ::ActiveRecord::Base.connection.update(update.to_sql)
150
+ end
151
+
152
+ def most_recent_transitions(most_recent_id = nil)
153
+ if most_recent_id
154
+ transitions_of_parent.and(
155
+ transition_table[:id].eq(most_recent_id).or(
156
+ transition_table[:most_recent].eq(true),
157
+ ),
158
+ )
113
159
  else
114
- most_recent.update_all(with_updated_timestamp(most_recent: nil))
160
+ transitions_of_parent.and(transition_table[:most_recent].eq(true))
161
+ end
162
+ end
163
+
164
+ def transitions_of_parent
165
+ transition_table[parent_join_foreign_key.to_sym].eq(parent_model.id)
166
+ end
167
+
168
+ # Generates update_all Arel values that will touch the updated timestamp (if valid
169
+ # for this model) and set most_recent to true only for the transition with a
170
+ # matching most_recent ID.
171
+ #
172
+ # This is quite nasty, but combines two updates (set all most_recent = f, set
173
+ # current most_recent = t) into one, which helps improve transition performance
174
+ # especially when database latency is significant.
175
+ #
176
+ # The SQL this can help produce looks like:
177
+ #
178
+ # update transitions
179
+ # set most_recent = (case when id = 'PA123' then TRUE else FALSE end)
180
+ # , updated_at = '...'
181
+ # ...
182
+ #
183
+ def build_most_recents_update_all_values(most_recent_id = nil)
184
+ [
185
+ [
186
+ transition_table[:most_recent],
187
+ Arel::Nodes::SqlLiteral.new(most_recent_value(most_recent_id)),
188
+ ],
189
+ ].tap do |values|
190
+ # Only if we support the updated at timestamps should we add this column to the
191
+ # update
192
+ updated_column, updated_at = updated_column_and_timestamp
193
+
194
+ if updated_column
195
+ values << [
196
+ transition_table[updated_column.to_sym],
197
+ updated_at,
198
+ ]
199
+ end
200
+ end
201
+ end
202
+
203
+ def most_recent_value(most_recent_id)
204
+ if most_recent_id
205
+ Arel::Nodes::Case.new.
206
+ when(transition_table[:id].eq(most_recent_id)).then(db_true).
207
+ else(not_most_recent_value).to_sql
208
+ else
209
+ Arel::Nodes::SqlLiteral.new(not_most_recent_value)
210
+ end
211
+ end
212
+
213
+ # Provide a wrapper for constructing an update manager which handles a breaking API
214
+ # change in Arel as we move into Rails >6.0.
215
+ #
216
+ # https://github.com/rails/rails/commit/7508284800f67b4611c767bff9eae7045674b66f
217
+ def build_arel_manager(manager)
218
+ if manager.instance_method(:initialize).arity.zero?
219
+ manager.new
220
+ else
221
+ manager.new(::ActiveRecord::Base)
115
222
  end
116
223
  end
117
224
 
@@ -170,7 +277,8 @@ module Statesman
170
277
  end
171
278
  end
172
279
 
173
- def with_updated_timestamp(params)
280
+ # updated_column_and_timestamp should return [column_name, value]
281
+ def updated_column_and_timestamp
174
282
  # TODO: Once we've set expectations that transition classes should conform to
175
283
  # the interface of Adapters::ActiveRecordTransition as a breaking change in the
176
284
  # next major version, we can stop calling `#respond_to?` first and instead
@@ -184,15 +292,51 @@ module Statesman
184
292
  ActiveRecordTransition::DEFAULT_UPDATED_TIMESTAMP_COLUMN
185
293
  end
186
294
 
187
- return params if column.nil?
295
+ # No updated timestamp column, don't return anything
296
+ return nil if column.nil?
188
297
 
189
- timestamp = if ::ActiveRecord::Base.default_timezone == :utc
190
- Time.now.utc
191
- else
192
- Time.now
193
- end
298
+ [
299
+ column, ::ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
300
+ ]
301
+ end
302
+
303
+ def mysql_gaplock_protection?
304
+ Statesman.mysql_gaplock_protection?
305
+ end
306
+
307
+ def db_true
308
+ value = ::ActiveRecord::Base.connection.type_cast(
309
+ true,
310
+ transition_class.columns_hash["most_recent"],
311
+ )
312
+ ::ActiveRecord::Base.connection.quote(value)
313
+ end
314
+
315
+ def db_false
316
+ value = ::ActiveRecord::Base.connection.type_cast(
317
+ false,
318
+ transition_class.columns_hash["most_recent"],
319
+ )
320
+ ::ActiveRecord::Base.connection.quote(value)
321
+ end
322
+
323
+ def db_null
324
+ Arel::Nodes::SqlLiteral.new("NULL")
325
+ end
326
+
327
+ # Check whether the `most_recent` column allows null values. If it doesn't, set old
328
+ # records to `false`, otherwise, set them to `NULL`.
329
+ #
330
+ # Some conditioning here is required to support databases that don't support partial
331
+ # indexes. By doing the conditioning on the column, rather than Rails' opinion of
332
+ # whether the database supports partial indexes, we're robust to DBs later adding
333
+ # support for partial indexes.
334
+ def not_most_recent_value(db_cast: true)
335
+ if transition_class.columns_hash["most_recent"].null == false
336
+ return db_cast ? db_false : false
337
+ end
194
338
 
195
- params.merge(column => timestamp)
339
+ db_cast ? db_null : nil
196
340
  end
197
341
  end
198
342
 
@@ -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
@@ -14,5 +14,31 @@ module Statesman
14
14
  def storage_adapter(adapter_class)
15
15
  @adapter_class = adapter_class
16
16
  end
17
+
18
+ def mysql_gaplock_protection?
19
+ return @mysql_gaplock_protection unless @mysql_gaplock_protection.nil?
20
+
21
+ # If our adapter class suggests we're using mysql, enable gaplock protection by
22
+ # default.
23
+ enable_mysql_gaplock_protection if mysql_adapter?(adapter_class)
24
+ @mysql_gaplock_protection
25
+ end
26
+
27
+ def enable_mysql_gaplock_protection
28
+ @mysql_gaplock_protection = true
29
+ end
30
+
31
+ private
32
+
33
+ def mysql_adapter?(adapter_class)
34
+ adapter_name = adapter_name(adapter_class)
35
+ return false unless adapter_name
36
+
37
+ adapter_name.start_with?("mysql")
38
+ end
39
+
40
+ def adapter_name(adapter_class)
41
+ adapter_class.respond_to?(:adapter_name) && adapter_class&.adapter_name
42
+ end
17
43
  end
18
44
  end
@@ -4,16 +4,21 @@ module Statesman
4
4
  class InvalidStateError < StandardError; end
5
5
  class InvalidTransitionError < StandardError; end
6
6
  class InvalidCallbackError < StandardError; end
7
+ class TransitionConflictError < StandardError; end
8
+ class MissingTransitionAssociation < StandardError; end
7
9
 
8
10
  class TransitionFailedError < StandardError
9
11
  def initialize(from, to)
10
12
  @from = from
11
13
  @to = to
14
+ super(_message)
12
15
  end
13
16
 
14
17
  attr_reader :from, :to
15
18
 
16
- def message
19
+ private
20
+
21
+ def _message
17
22
  "Cannot transition from '#{from}' to '#{to}'"
18
23
  end
19
24
  end
@@ -22,18 +27,18 @@ module Statesman
22
27
  def initialize(from, to)
23
28
  @from = from
24
29
  @to = to
30
+ super(_message)
25
31
  end
26
32
 
27
33
  attr_reader :from, :to
28
34
 
29
- def message
35
+ private
36
+
37
+ def _message
30
38
  "Guard on transition from: '#{from}' to '#{to}' returned false"
31
39
  end
32
40
  end
33
41
 
34
- class TransitionConflictError < StandardError; end
35
- class MissingTransitionAssociation < StandardError; end
36
-
37
42
  class UnserializedMetadataError < StandardError
38
43
  def initialize(transition_class_name)
39
44
  super(_message(transition_class_name))
@@ -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.0.1"
4
+ VERSION = "7.4.1"
5
5
  end
@@ -20,7 +20,9 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
20
20
  prepare_other_model_table
21
21
  prepare_other_transitions_table
22
22
 
23
- Statesman.configure { storage_adapter(Statesman::Adapters::ActiveRecord) }
23
+ Statesman.configure do
24
+ storage_adapter(Statesman::Adapters::ActiveRecord)
25
+ end
24
26
  end
25
27
 
26
28
  after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }
@@ -9,9 +9,19 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
9
9
  before do
10
10
  prepare_model_table
11
11
  prepare_transitions_table
12
+
13
+ MyActiveRecordModelTransition.serialize(:metadata, JSON)
14
+
15
+ Statesman.configure do
16
+ # Rubocop requires described_class to be used, but this block
17
+ # is instance_eval'd and described_class won't be defined
18
+ # rubocop:disable RSpec/DescribedClass
19
+ storage_adapter(Statesman::Adapters::ActiveRecord)
20
+ # rubocop:enable RSpec/DescribedClass
21
+ end
12
22
  end
13
23
 
14
- before { MyActiveRecordModelTransition.serialize(:metadata, JSON) }
24
+ after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }
15
25
 
16
26
  let(:observer) { double(Statesman::Machine, execute: nil) }
17
27
  let(:model) { MyActiveRecordModel.create(current_state: :pending) }
@@ -345,6 +355,19 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
345
355
  end
346
356
  end
347
357
 
358
+ it "resets last with #reload" do
359
+ model.save!
360
+ ActiveRecord::Base.transaction do
361
+ model.state_machine.transition_to!(:succeeded)
362
+ # force to cache value in last_transition instance variable
363
+ expect(model.state_machine.current_state).to eq("succeeded")
364
+ raise ActiveRecord::Rollback
365
+ end
366
+ expect(model.state_machine.current_state).to eq("succeeded")
367
+ model.reload
368
+ expect(model.state_machine.current_state).to eq("initial")
369
+ end
370
+
348
371
  context "with a namespaced model" do
349
372
  before do
350
373
  CreateNamespacedARModelMigration.migrate(:up)
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe Statesman do
6
+ describe "InvalidStateError" do
7
+ subject(:error) { Statesman::InvalidStateError.new }
8
+
9
+ its(:message) { is_expected.to eq("Statesman::InvalidStateError") }
10
+ its "string matches its message" do
11
+ expect(error.to_s).to eq(error.message)
12
+ end
13
+ end
14
+
15
+ describe "InvalidTransitionError" do
16
+ subject(:error) { Statesman::InvalidTransitionError.new }
17
+
18
+ its(:message) { is_expected.to eq("Statesman::InvalidTransitionError") }
19
+ its "string matches its message" do
20
+ expect(error.to_s).to eq(error.message)
21
+ end
22
+ end
23
+
24
+ describe "InvalidCallbackError" do
25
+ subject(:error) { Statesman::InvalidTransitionError.new }
26
+
27
+ its(:message) { is_expected.to eq("Statesman::InvalidTransitionError") }
28
+ its "string matches its message" do
29
+ expect(error.to_s).to eq(error.message)
30
+ end
31
+ end
32
+
33
+ describe "TransitionConflictError" do
34
+ subject(:error) { Statesman::TransitionConflictError.new }
35
+
36
+ its(:message) { is_expected.to eq("Statesman::TransitionConflictError") }
37
+ its "string matches its message" do
38
+ expect(error.to_s).to eq(error.message)
39
+ end
40
+ end
41
+
42
+ describe "MissingTransitionAssociation" do
43
+ subject(:error) { Statesman::MissingTransitionAssociation.new }
44
+
45
+ its(:message) { is_expected.to eq("Statesman::MissingTransitionAssociation") }
46
+ its "string matches its message" do
47
+ expect(error.to_s).to eq(error.message)
48
+ end
49
+ end
50
+
51
+ describe "TransitionFailedError" do
52
+ subject(:error) { Statesman::TransitionFailedError.new("from", "to") }
53
+
54
+ its(:message) { is_expected.to eq("Cannot transition from 'from' to 'to'") }
55
+ its "string matches its message" do
56
+ expect(error.to_s).to eq(error.message)
57
+ end
58
+ end
59
+
60
+ describe "GuardFailedError" do
61
+ subject(:error) { Statesman::GuardFailedError.new("from", "to") }
62
+
63
+ its(:message) do
64
+ is_expected.to eq("Guard on transition from: 'from' to 'to' returned false")
65
+ end
66
+ its "string matches its message" do
67
+ expect(error.to_s).to eq(error.message)
68
+ end
69
+ end
70
+
71
+ describe "UnserializedMetadataError" do
72
+ subject(:error) { Statesman::UnserializedMetadataError.new("foo") }
73
+
74
+ its(:message) { is_expected.to match(/foo#metadata is not serialized/) }
75
+ its "string matches its message" do
76
+ expect(error.to_s).to eq(error.message)
77
+ end
78
+ end
79
+
80
+ describe "IncompatibleSerializationError" do
81
+ subject(:error) { Statesman::IncompatibleSerializationError.new("foo") }
82
+
83
+ its(:message) { is_expected.to match(/foo#metadata column type cannot be json/) }
84
+ its "string matches its message" do
85
+ expect(error.to_s).to eq(error.message)
86
+ end
87
+ end
88
+ end
@@ -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
@@ -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)
@@ -25,7 +27,7 @@ Gem::Specification.new do |spec|
25
27
  spec.add_development_dependency "bundler", "~> 2.1.4"
26
28
  spec.add_development_dependency "gc_ruboconfig", "~> 2.3.9"
27
29
  spec.add_development_dependency "mysql2", ">= 0.4", "< 0.6"
28
- spec.add_development_dependency "pg", "~> 0.18"
30
+ spec.add_development_dependency "pg", ">= 0.18", "<= 1.3"
29
31
  spec.add_development_dependency "pry"
30
32
  spec.add_development_dependency "rails", ">= 5.2"
31
33
  spec.add_development_dependency "rake", "~> 13.0.0"
@@ -33,6 +35,14 @@ Gem::Specification.new do |spec|
33
35
  spec.add_development_dependency "rspec-its", "~> 1.1"
34
36
  spec.add_development_dependency "rspec-rails", "~> 3.1"
35
37
  spec.add_development_dependency "rspec_junit_formatter", "~> 0.4.0"
36
- spec.add_development_dependency "sqlite3", "~> 1.3.6"
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.0.1
4
+ version: 7.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - GoCardless
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-08 00:00:00.000000000 Z
11
+ date: 2020-11-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ammeter
@@ -76,16 +76,22 @@ dependencies:
76
76
  name: pg
77
77
  requirement: !ruby/object:Gem::Requirement
78
78
  requirements:
79
- - - "~>"
79
+ - - ">="
80
80
  - !ruby/object:Gem::Version
81
81
  version: '0.18'
82
+ - - "<="
83
+ - !ruby/object:Gem::Version
84
+ version: '1.3'
82
85
  type: :development
83
86
  prerelease: false
84
87
  version_requirements: !ruby/object:Gem::Requirement
85
88
  requirements:
86
- - - "~>"
89
+ - - ">="
87
90
  - !ruby/object:Gem::Version
88
91
  version: '0.18'
92
+ - - "<="
93
+ - !ruby/object:Gem::Version
94
+ version: '1.3'
89
95
  - !ruby/object:Gem::Dependency
90
96
  name: pry
91
97
  requirement: !ruby/object:Gem::Requirement
@@ -190,14 +196,14 @@ dependencies:
190
196
  requirements:
191
197
  - - "~>"
192
198
  - !ruby/object:Gem::Version
193
- version: 1.3.6
199
+ version: 1.4.2
194
200
  type: :development
195
201
  prerelease: false
196
202
  version_requirements: !ruby/object:Gem::Requirement
197
203
  requirements:
198
204
  - - "~>"
199
205
  - !ruby/object:Gem::Version
200
- version: 1.3.6
206
+ version: 1.4.2
201
207
  - !ruby/object:Gem::Dependency
202
208
  name: timecop
203
209
  requirement: !ruby/object:Gem::Requirement
@@ -266,6 +272,7 @@ files:
266
272
  - spec/statesman/adapters/shared_examples.rb
267
273
  - spec/statesman/callback_spec.rb
268
274
  - spec/statesman/config_spec.rb
275
+ - spec/statesman/exceptions_spec.rb
269
276
  - spec/statesman/guard_spec.rb
270
277
  - spec/statesman/machine_spec.rb
271
278
  - spec/statesman/utils_spec.rb
@@ -275,8 +282,13 @@ files:
275
282
  homepage: https://github.com/gocardless/statesman
276
283
  licenses:
277
284
  - MIT
278
- metadata: {}
279
- post_install_message:
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
291
+ post_install_message:
280
292
  rdoc_options: []
281
293
  require_paths:
282
294
  - lib
@@ -291,8 +303,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
291
303
  - !ruby/object:Gem::Version
292
304
  version: '0'
293
305
  requirements: []
294
- rubygems_version: 3.1.2
295
- signing_key:
306
+ rubygems_version: 3.2.0.rc.1
307
+ signing_key:
296
308
  specification_version: 4
297
309
  summary: A statesman-like state machine library
298
310
  test_files:
@@ -310,6 +322,7 @@ test_files:
310
322
  - spec/statesman/adapters/shared_examples.rb
311
323
  - spec/statesman/callback_spec.rb
312
324
  - spec/statesman/config_spec.rb
325
+ - spec/statesman/exceptions_spec.rb
313
326
  - spec/statesman/guard_spec.rb
314
327
  - spec/statesman/machine_spec.rb
315
328
  - spec/statesman/utils_spec.rb