statesman 7.0.0 → 7.4.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: 3e43186830b499468b2a68209d617f21c2d69039ce77e2e91182de63b617bd63
4
- data.tar.gz: 1bd2c4b13efbed3525e9bb03fe301c6695b21d4d6228a906276acbb0198010d4
3
+ metadata.gz: f0dbcc9fe3c29e715be585f2ddae96bdea9b80ca104daf00ee3378feab681b8e
4
+ data.tar.gz: c5824ece991650b8e6e979a9899dbdabc38b18209a79260f2aa9e9f4fda67da2
5
5
  SHA512:
6
- metadata.gz: 9caf345cd2c806be717407e9ee4cf4a15e2bec5e583f7b5f2ba1bee7cf6ef531d2e32a0cf9196cc1e2af0e2a168e584c09d5eea076a0381837cd769bf13f9814
7
- data.tar.gz: af376684100920894aca78bbee1cb4e7e17bc3202bb8aa649a29c40789f3545455e78c9d61ceb6dc99e37aafd5b8f78421cd8813e53c7e77c099a46aac150968
6
+ metadata.gz: 98fe2966e48e912b33f1c6d842aa6090782dd32be726532d8178377ffb0f1a551ee3d83eee291de0723c756eb0b1bda65dae73d695a47f8b6bd38106b127615f
7
+ data.tar.gz: 6be77a7114e2c3d59e76cfa71de583213ead8a171c5039941698c1e4b0b28b68500acba91d498fb488d33d0e31b3274fa5e373ce81a55f9f70e8e229132de1cf
@@ -12,7 +12,7 @@ references:
12
12
  - type: cache-restore
13
13
  key: statesman-{{ checksum "Gemfile" }}-{{ checksum "~/RAILS_VERSION.txt" }}
14
14
 
15
- - run: gem install bundler -v 1.3
15
+ - run: gem install bundler -v 2.1.4
16
16
 
17
17
  - run: bundle install --path vendor/bundle
18
18
 
@@ -31,6 +31,34 @@ references:
31
31
  path: /tmp/test-results
32
32
 
33
33
  jobs:
34
+ build-ruby249-rails-524-mysql:
35
+ docker:
36
+ - image: circleci/ruby:2.4.9-node
37
+ environment:
38
+ - RAILS_VERSION=5.2.4
39
+ - DATABASE_URL=mysql2://root@127.0.0.1/statesman_test
40
+ - DATABASE_DEPENDENCY_PORT=3306
41
+ - image: circleci/mysql:5.7.18
42
+ environment:
43
+ - MYSQL_ALLOW_EMPTY_PASSWORD=true
44
+ - MYSQL_USER=root
45
+ - MYSQL_PASSWORD=
46
+ - MYSQL_DATABASE=statesman_test
47
+ steps: *steps
48
+ build-ruby249-rails-524-postgres:
49
+ docker:
50
+ - image: circleci/ruby:2.4.9-node
51
+ environment:
52
+ - RAILS_VERSION=5.2.4
53
+ - DATABASE_URL=postgres://postgres@localhost/statesman_test
54
+ - DATABASE_DEPENDENCY_PORT=5432
55
+ - image: circleci/postgres:9.6
56
+ environment:
57
+ - POSTGRES_USER=postgres
58
+ - POSTGRES_DB=statesman_test
59
+ - POSTGRES_PASSWORD=statesman
60
+ steps: *steps
61
+
34
62
  build-ruby265-rails-602-mysql:
35
63
  docker:
36
64
  - image: circleci/ruby:2.6.5-node
@@ -56,6 +84,7 @@ jobs:
56
84
  environment:
57
85
  - POSTGRES_USER=postgres
58
86
  - POSTGRES_DB=statesman_test
87
+ - POSTGRES_PASSWORD=statesman
59
88
  steps: *steps
60
89
  build-ruby265-rails-master-mysql:
61
90
  docker:
@@ -83,12 +112,14 @@ jobs:
83
112
  environment:
84
113
  - POSTGRES_USER=postgres
85
114
  - POSTGRES_DB=statesman_test
115
+ - POSTGRES_PASSWORD=statesman
86
116
  steps: *steps
87
- build-ruby249-rails-524-mysql:
117
+
118
+ build-ruby270-rails-602-mysql:
88
119
  docker:
89
- - image: circleci/ruby:2.4.9-node
120
+ - image: circleci/ruby:2.7.0-node
90
121
  environment:
91
- - RAILS_VERSION=5.2.4
122
+ - RAILS_VERSION=6.0.2
92
123
  - DATABASE_URL=mysql2://root@127.0.0.1/statesman_test
93
124
  - DATABASE_DEPENDENCY_PORT=3306
94
125
  - image: circleci/mysql:5.7.18
@@ -98,19 +129,47 @@ jobs:
98
129
  - MYSQL_PASSWORD=
99
130
  - MYSQL_DATABASE=statesman_test
100
131
  steps: *steps
101
- build-ruby249-rails-524-postgres:
132
+ build-ruby270-rails-602-postgres:
102
133
  docker:
103
- - image: circleci/ruby:2.4.9-node
134
+ - image: circleci/ruby:2.7.0-node
104
135
  environment:
105
- - RAILS_VERSION=5.2.4
136
+ - RAILS_VERSION=6.0.2
106
137
  - DATABASE_URL=postgres://postgres@localhost/statesman_test
107
138
  - DATABASE_DEPENDENCY_PORT=5432
108
139
  - image: circleci/postgres:9.6
109
140
  environment:
110
141
  - POSTGRES_USER=postgres
111
142
  - POSTGRES_DB=statesman_test
143
+ - POSTGRES_PASSWORD=statesman
144
+ steps: *steps
145
+ build-ruby270-rails-master-mysql:
146
+ docker:
147
+ - image: circleci/ruby:2.7.0-node
148
+ environment:
149
+ - RAILS_VERSION=master
150
+ - DATABASE_URL=mysql2://root@127.0.0.1/statesman_test
151
+ - DATABASE_DEPENDENCY_PORT=3306
152
+ - image: circleci/mysql:5.7.18
153
+ environment:
154
+ - MYSQL_ALLOW_EMPTY_PASSWORD=true
155
+ - MYSQL_USER=root
156
+ - MYSQL_PASSWORD=
157
+ - MYSQL_DATABASE=statesman_test
158
+ steps: *steps
159
+ build-ruby270-rails-master-postgres:
160
+ docker:
161
+ - image: circleci/ruby:2.7.0-node
162
+ environment:
163
+ - RAILS_VERSION=master
164
+ - DATABASE_URL=postgres://postgres@localhost/statesman_test
165
+ - EXCLUDE_MONGOID=true
166
+ - DATABASE_DEPENDENCY_PORT=5432
167
+ - image: circleci/postgres:9.6
168
+ environment:
169
+ - POSTGRES_USER=postgres
170
+ - POSTGRES_DB=statesman_test
171
+ - POSTGRES_PASSWORD=statesman
112
172
  steps: *steps
113
-
114
173
 
115
174
  workflows:
116
175
  version: 2
@@ -122,3 +181,7 @@ workflows:
122
181
  - build-ruby265-rails-602-postgres
123
182
  - build-ruby265-rails-master-mysql
124
183
  - build-ruby265-rails-master-postgres
184
+ - build-ruby270-rails-602-mysql
185
+ - build-ruby270-rails-602-postgres
186
+ - build-ruby270-rails-master-mysql
187
+ - build-ruby270-rails-master-postgres
@@ -1,3 +1,34 @@
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
+
28
+ ## v7.0.1, 8th Jan 2020
29
+
30
+ - Fix deprecation warning with Ruby 2.7 [#386](https://github.com/gocardless/statesman/pull/386)
31
+
1
32
  ## v7.0.0, 8th Jan 2020
2
33
 
3
34
  **Breaking changes**
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', '~> 5.2.0'
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
@@ -65,19 +69,36 @@ module Statesman
65
69
 
66
70
  private
67
71
 
72
+ # rubocop:disable Metrics/MethodLength
68
73
  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)
74
+ transition = transitions_for_parent.build(
75
+ default_transition_attributes(to, metadata),
76
+ )
76
77
 
77
78
  ::ActiveRecord::Base.transaction(requires_new: true) do
78
79
  @observer.execute(:before, from, to, transition)
79
- unset_old_most_recent
80
- transition.save!
80
+
81
+ if mysql_gaplock_protection?
82
+ # We save the transition first with most_recent falsy, then mark most_recent
83
+ # true after to avoid letting MySQL acquire a next-key lock which can cause
84
+ # deadlocks.
85
+ #
86
+ # To avoid an additional query, we manually adjust the most_recent attribute
87
+ # on our transition assuming that update_most_recents will have set it to true
88
+
89
+ transition.save!
90
+
91
+ unless update_most_recents(transition.id).positive?
92
+ raise ActiveRecord::Rollback, "failed to update most_recent"
93
+ end
94
+
95
+ transition.assign_attributes(most_recent: true)
96
+ else
97
+ update_most_recents
98
+ transition.assign_attributes(most_recent: true)
99
+ transition.save!
100
+ end
101
+
81
102
  @last_transition = transition
82
103
  @observer.execute(:after, from, to, transition)
83
104
  add_after_commit_callback(from, to, transition)
@@ -85,6 +106,16 @@ module Statesman
85
106
 
86
107
  transition
87
108
  end
109
+ # rubocop:enable Metrics/MethodLength
110
+
111
+ def default_transition_attributes(to, metadata)
112
+ {
113
+ to_state: to,
114
+ sort_key: next_sort_key,
115
+ metadata: metadata,
116
+ most_recent: not_most_recent_value(db_cast: false),
117
+ }
118
+ end
88
119
 
89
120
  def add_after_commit_callback(from, to, transition)
90
121
  ::ActiveRecord::Base.connection.add_transaction_record(
@@ -98,20 +129,92 @@ module Statesman
98
129
  parent_model.send(@association_name)
99
130
  end
100
131
 
101
- def unset_old_most_recent
102
- most_recent = transitions_for_parent.where(most_recent: true)
132
+ # Sets the given transition most_recent = t while unsetting the most_recent of any
133
+ # previous transitions.
134
+ def update_most_recents(most_recent_id = nil)
135
+ update = build_arel_manager(::Arel::UpdateManager)
136
+ update.table(transition_table)
137
+ update.where(most_recent_transitions(most_recent_id))
138
+ update.set(build_most_recents_update_all_values(most_recent_id))
103
139
 
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))
140
+ # MySQL will validate index constraints across the intermediate result of an
141
+ # update. This means we must order our update to deactivate the previous
142
+ # most_recent before setting the new row to be true.
143
+ update.order(transition_table[:most_recent].desc) if mysql_gaplock_protection?
144
+
145
+ ::ActiveRecord::Base.connection.update(update.to_sql)
146
+ end
147
+
148
+ def most_recent_transitions(most_recent_id = nil)
149
+ if most_recent_id
150
+ transitions_of_parent.and(
151
+ transition_table[:id].eq(most_recent_id).or(
152
+ transition_table[:most_recent].eq(true),
153
+ ),
154
+ )
155
+ else
156
+ transitions_of_parent.and(transition_table[:most_recent].eq(true))
157
+ end
158
+ end
159
+
160
+ def transitions_of_parent
161
+ transition_table[parent_join_foreign_key.to_sym].eq(parent_model.id)
162
+ end
163
+
164
+ # Generates update_all Arel values that will touch the updated timestamp (if valid
165
+ # for this model) and set most_recent to true only for the transition with a
166
+ # matching most_recent ID.
167
+ #
168
+ # This is quite nasty, but combines two updates (set all most_recent = f, set
169
+ # current most_recent = t) into one, which helps improve transition performance
170
+ # especially when database latency is significant.
171
+ #
172
+ # The SQL this can help produce looks like:
173
+ #
174
+ # update transitions
175
+ # set most_recent = (case when id = 'PA123' then TRUE else FALSE end)
176
+ # , updated_at = '...'
177
+ # ...
178
+ #
179
+ def build_most_recents_update_all_values(most_recent_id = nil)
180
+ [
181
+ [
182
+ transition_table[:most_recent],
183
+ Arel::Nodes::SqlLiteral.new(most_recent_value(most_recent_id)),
184
+ ],
185
+ ].tap do |values|
186
+ # Only if we support the updated at timestamps should we add this column to the
187
+ # update
188
+ updated_column, updated_at = updated_column_and_timestamp
189
+
190
+ if updated_column
191
+ values << [
192
+ transition_table[updated_column.to_sym],
193
+ updated_at,
194
+ ]
195
+ end
196
+ end
197
+ end
198
+
199
+ def most_recent_value(most_recent_id)
200
+ if most_recent_id
201
+ Arel::Nodes::Case.new.
202
+ when(transition_table[:id].eq(most_recent_id)).then(db_true).
203
+ else(not_most_recent_value).to_sql
113
204
  else
114
- most_recent.update_all(with_updated_timestamp(most_recent: nil))
205
+ Arel::Nodes::SqlLiteral.new(not_most_recent_value)
206
+ end
207
+ end
208
+
209
+ # Provide a wrapper for constructing an update manager which handles a breaking API
210
+ # change in Arel as we move into Rails >6.0.
211
+ #
212
+ # https://github.com/rails/rails/commit/7508284800f67b4611c767bff9eae7045674b66f
213
+ def build_arel_manager(manager)
214
+ if manager.instance_method(:initialize).arity.zero?
215
+ manager.new
216
+ else
217
+ manager.new(::ActiveRecord::Base)
115
218
  end
116
219
  end
117
220
 
@@ -170,7 +273,8 @@ module Statesman
170
273
  end
171
274
  end
172
275
 
173
- def with_updated_timestamp(params)
276
+ # updated_column_and_timestamp should return [column_name, value]
277
+ def updated_column_and_timestamp
174
278
  # TODO: Once we've set expectations that transition classes should conform to
175
279
  # the interface of Adapters::ActiveRecordTransition as a breaking change in the
176
280
  # next major version, we can stop calling `#respond_to?` first and instead
@@ -184,21 +288,57 @@ module Statesman
184
288
  ActiveRecordTransition::DEFAULT_UPDATED_TIMESTAMP_COLUMN
185
289
  end
186
290
 
187
- return params if column.nil?
291
+ # No updated timestamp column, don't return anything
292
+ return nil if column.nil?
293
+
294
+ [
295
+ column, ::ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
296
+ ]
297
+ end
298
+
299
+ def mysql_gaplock_protection?
300
+ Statesman.mysql_gaplock_protection?
301
+ end
302
+
303
+ def db_true
304
+ value = ::ActiveRecord::Base.connection.type_cast(
305
+ true,
306
+ transition_class.columns_hash["most_recent"],
307
+ )
308
+ ::ActiveRecord::Base.connection.quote(value)
309
+ end
188
310
 
189
- timestamp = if ::ActiveRecord::Base.default_timezone == :utc
190
- Time.now.utc
191
- else
192
- Time.now
193
- end
311
+ def db_false
312
+ value = ::ActiveRecord::Base.connection.type_cast(
313
+ false,
314
+ transition_class.columns_hash["most_recent"],
315
+ )
316
+ ::ActiveRecord::Base.connection.quote(value)
317
+ end
318
+
319
+ def db_null
320
+ Arel::Nodes::SqlLiteral.new("NULL")
321
+ end
322
+
323
+ # Check whether the `most_recent` column allows null values. If it doesn't, set old
324
+ # records to `false`, otherwise, set them to `NULL`.
325
+ #
326
+ # Some conditioning here is required to support databases that don't support partial
327
+ # indexes. By doing the conditioning on the column, rather than Rails' opinion of
328
+ # whether the database supports partial indexes, we're robust to DBs later adding
329
+ # support for partial indexes.
330
+ def not_most_recent_value(db_cast: true)
331
+ if transition_class.columns_hash["most_recent"].null == false
332
+ return db_cast ? db_false : false
333
+ end
194
334
 
195
- params.merge(column => timestamp)
335
+ db_cast ? db_null : nil
196
336
  end
197
337
  end
198
338
 
199
339
  class ActiveRecordAfterCommitWrap
200
- def initialize
201
- @callback = Proc.new
340
+ def initialize(&block)
341
+ @callback = block
202
342
  @connection = ::ActiveRecord::Base.connection
203
343
  end
204
344
 
@@ -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))
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Statesman
4
- VERSION = "7.0.0"
4
+ VERSION = "7.4.0"
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) }
@@ -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
@@ -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)
@@ -22,10 +24,10 @@ Gem::Specification.new do |spec|
22
24
  spec.required_ruby_version = ">= 2.2"
23
25
 
24
26
  spec.add_development_dependency "ammeter", "~> 1.1"
25
- spec.add_development_dependency "bundler", "~> 1.3"
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.0
4
+ version: 7.4.0
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-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ammeter
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.3'
33
+ version: 2.1.4
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.3'
40
+ version: 2.1.4
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: gc_ruboconfig
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -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.1.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