statesman 7.0.1 → 7.4.1

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