statesman 3.5.0 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +45 -225
  3. data/.rubocop.yml +1 -1
  4. data/.rubocop_todo.yml +26 -6
  5. data/CHANGELOG.md +69 -0
  6. data/Gemfile +9 -3
  7. data/Guardfile +2 -0
  8. data/README.md +77 -47
  9. data/Rakefile +2 -4
  10. data/lib/generators/statesman/active_record_transition_generator.rb +2 -0
  11. data/lib/generators/statesman/generator_helpers.rb +2 -0
  12. data/lib/generators/statesman/migration_generator.rb +2 -0
  13. data/lib/statesman/adapters/active_record.rb +88 -6
  14. data/lib/statesman/adapters/active_record_queries.rb +100 -36
  15. data/lib/statesman/adapters/active_record_transition.rb +2 -0
  16. data/lib/statesman/adapters/memory.rb +2 -0
  17. data/lib/statesman/adapters/memory_transition.rb +2 -0
  18. data/lib/statesman/callback.rb +2 -0
  19. data/lib/statesman/config.rb +2 -0
  20. data/lib/statesman/exceptions.rb +29 -2
  21. data/lib/statesman/guard.rb +3 -4
  22. data/lib/statesman/machine.rb +29 -7
  23. data/lib/statesman/railtie.rb +2 -0
  24. data/lib/statesman/utils.rb +2 -0
  25. data/lib/statesman/version.rb +3 -1
  26. data/lib/statesman.rb +2 -3
  27. data/lib/tasks/statesman.rake +3 -1
  28. data/spec/fixtures/add_constraints_to_most_recent_for_bacon_transitions_with_partial_index.rb +2 -0
  29. data/spec/fixtures/add_constraints_to_most_recent_for_bacon_transitions_without_partial_index.rb +2 -0
  30. data/spec/fixtures/add_most_recent_to_bacon_transitions.rb +2 -0
  31. data/spec/generators/statesman/active_record_transition_generator_spec.rb +2 -0
  32. data/spec/generators/statesman/migration_generator_spec.rb +2 -0
  33. data/spec/spec_helper.rb +3 -30
  34. data/spec/statesman/adapters/active_record_queries_spec.rb +165 -91
  35. data/spec/statesman/adapters/active_record_spec.rb +4 -0
  36. data/spec/statesman/adapters/active_record_transition_spec.rb +2 -0
  37. data/spec/statesman/adapters/memory_spec.rb +2 -0
  38. data/spec/statesman/adapters/memory_transition_spec.rb +2 -0
  39. data/spec/statesman/adapters/shared_examples.rb +2 -0
  40. data/spec/statesman/callback_spec.rb +2 -0
  41. data/spec/statesman/config_spec.rb +2 -0
  42. data/spec/statesman/guard_spec.rb +2 -0
  43. data/spec/statesman/machine_spec.rb +79 -4
  44. data/spec/statesman/utils_spec.rb +2 -0
  45. data/spec/support/active_record.rb +9 -12
  46. data/spec/support/generators_shared_examples.rb +2 -0
  47. data/statesman.gemspec +5 -3
  48. metadata +17 -22
  49. data/lib/generators/statesman/mongoid_transition_generator.rb +0 -25
  50. data/lib/generators/statesman/templates/mongoid_transition_model.rb.erb +0 -14
  51. data/lib/statesman/adapters/mongoid.rb +0 -66
  52. data/lib/statesman/adapters/mongoid_transition.rb +0 -10
  53. data/spec/generators/statesman/mongoid_transition_generator_spec.rb +0 -23
  54. data/spec/statesman/adapters/mongoid_spec.rb +0 -86
  55. data/spec/support/mongoid.rb +0 -28
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ![Statesman](http://f.cl.ly/items/410n2A0S3l1W0i3i0o2K/statesman.png)
1
+ <p align="center"><img src="http://f.cl.ly/items/410n2A0S3l1W0i3i0o2K/statesman.png" alt="Statesman"></p>
2
2
 
3
3
  A statesmanlike state machine library.
4
4
 
@@ -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', '~> 3.4.1'
33
+ gem 'statesman', '~> 5.2.0'
34
34
  ```
35
35
 
36
36
  ## Usage
@@ -76,22 +76,16 @@ Then, link it to your model:
76
76
 
77
77
  ```ruby
78
78
  class Order < ActiveRecord::Base
79
- include Statesman::Adapters::ActiveRecordQueries
80
-
81
79
  has_many :order_transitions, autosave: false
82
80
 
81
+ include Statesman::Adapters::ActiveRecordQueries[
82
+ transition_class: OrderTransition,
83
+ initial_state: :pending
84
+ ]
85
+
83
86
  def state_machine
84
87
  @state_machine ||= OrderStateMachine.new(self, transition_class: OrderTransition)
85
88
  end
86
-
87
- def self.transition_class
88
- OrderTransition
89
- end
90
-
91
- def self.initial_state
92
- :pending
93
- end
94
- private_class_method :initial_state
95
89
  end
96
90
  ```
97
91
 
@@ -164,8 +158,9 @@ class Order < ActiveRecord::Base
164
158
  end
165
159
 
166
160
  # Optionally delegate some methods
167
- delegate :can_transition_to?, :transition_to!, :transition_to, :current_state,
168
- to: :state_machine
161
+
162
+ delegate :can_transition_to?, :current_state, :history, :last_transition,
163
+ :transition_to!, :transition_to, :in_state?, to: :state_machine
169
164
  end
170
165
  ```
171
166
  #### Using PostgreSQL JSON column
@@ -199,13 +194,11 @@ or 5. To do that
199
194
  ```ruby
200
195
  Statesman.configure do
201
196
  storage_adapter(Statesman::Adapters::ActiveRecord)
202
- # ...or
203
- storage_adapter(Statesman::Adapters::Mongoid)
204
197
  end
205
198
  ```
206
199
  Statesman defaults to storing transitions in memory. If you're using rails, you
207
200
  can instead configure it to persist transitions to the database by using the
208
- ActiveRecord or Mongoid adapter.
201
+ ActiveRecord adapter.
209
202
 
210
203
  Statesman will fallback to memory unless you specify a transition_class when instantiating your state machine. This allows you to only persist transitions on certain state machines in your app.
211
204
 
@@ -234,7 +227,8 @@ end
234
227
  ```
235
228
  Define a guard. `to` and `from` parameters are optional, a nil parameter means
236
229
  guard all transitions. The passed block should evaluate to a boolean and must
237
- be idempotent as it could be called many times.
230
+ be idempotent as it could be called many times. The guard will pass when it
231
+ evaluates to a truthy value and fail when it evaluates to a falsey value (`nil` or `false`).
238
232
 
239
233
  #### `Machine.before_transition`
240
234
  ```ruby
@@ -261,6 +255,35 @@ after the transition.
261
255
  If you specify `after_commit: true`, the callback will be executed once the
262
256
  transition has been committed to the database.
263
257
 
258
+ #### `Machine.after_transition_failure`
259
+ ```ruby
260
+ Machine.after_transition_failure(from: :some_state, to: :another_state) do |object, exception|
261
+ Logger.info("transition to #{exception.to} failed for #{object.id}")
262
+ end
263
+ ```
264
+ Define a callback to run if `Statesman::TransitionFailedError` is raised
265
+ during the execution of transition callbacks. `to` and `from`
266
+ parameters are optional, a nil parameter means run after all transitions.
267
+ The model object, and exception are passed as arguments to the callback.
268
+ This is executed outside of the transaction wrapping other callbacks.
269
+ If using `transition!` the exception is re-raised after these callbacks are
270
+ executed.
271
+
272
+ #### `Machine.after_guard_failure`
273
+ ```ruby
274
+ Machine.after_guard_failure(from: :some_state, to: :another_state) do |object, exception|
275
+ Logger.info("guard failed during transition to #{exception.to} for #{object.id}")
276
+ end
277
+ ```
278
+ Define a callback to run if `Statesman::GuardFailedError` is raised
279
+ during the execution of guard callbacks. `to` and `from`
280
+ parameters are optional, a nil parameter means run after all transitions.
281
+ The model object, and exception are passed as arguments to the callback.
282
+ This is executed outside of the transaction wrapping other callbacks.
283
+ If using `transition!` the exception is re-raised after these callbacks are
284
+ executed.
285
+
286
+
264
287
  #### `Machine.new`
265
288
  ```ruby
266
289
  my_machine = Machine.new(my_model, transition_class: MyTransitionModel)
@@ -328,43 +351,34 @@ callback code throws an exception, it will not be caught.)
328
351
 
329
352
  A mixin is provided for the ActiveRecord adapter which adds scopes to easily
330
353
  find all models currently in (or not in) a given state. Include it into your
331
- model and define `transition_class` and `initial_state` class methods:
354
+ model and passing in `transition_class` and `initial_state` as options.
355
+
356
+ In 4.1.2 and below, these two options had to be defined as methods on the model,
357
+ but 5.0.0 and above allow this style of configuration as well.
358
+ The old method pollutes the model with extra class methods, and is deprecated,
359
+ to be removed in 6.0.0.
332
360
 
333
361
  ```ruby
334
362
  class Order < ActiveRecord::Base
335
- include Statesman::Adapters::ActiveRecordQueries
336
-
337
- def self.transition_class
338
- OrderTransition
339
- end
340
- private_class_method :transition_class
341
-
342
- def self.initial_state
343
- OrderStateMachine.initial_state
344
- end
345
- private_class_method :initial_state
363
+ has_many :order_transitions, autosave: false
364
+ include Statesman::Adapters::ActiveRecordQueries[
365
+ transition_class: OrderTransition,
366
+ initial_state: OrderStateMachine.initial_state
367
+ ]
346
368
  end
347
369
  ```
348
370
 
349
371
  If the transition class-name differs from the association name, you will also
350
- need to define a corresponding `transition_name` class method:
372
+ need to pass `transition_name` as an option:
351
373
 
352
374
  ```ruby
353
375
  class Order < ActiveRecord::Base
354
376
  has_many :transitions, class_name: "OrderTransition", autosave: false
355
-
356
- def self.transition_name
357
- :transitions
358
- end
359
-
360
- def self.transition_class
361
- OrderTransition
362
- end
363
-
364
- def self.initial_state
365
- OrderStateMachine.initial_state
366
- end
367
- private_class_method :initial_state
377
+ include Statesman::Adapters::ActiveRecordQueries[
378
+ transition_class: OrderTransition,
379
+ initial_state: OrderStateMachine.initial_state,
380
+ transition_name: :transitions
381
+ ]
368
382
  end
369
383
  ```
370
384
 
@@ -375,7 +389,7 @@ Returns all models currently in any of the supplied states.
375
389
  Returns all models not currently in any of the supplied states.
376
390
 
377
391
 
378
- ### `Model.most_recent_transition_join`
392
+ #### `Model.most_recent_transition_join`
379
393
  This joins the model to its most recent transition whatever that may be.
380
394
  We expose this method to ease use of ActiveRecord's `or` e.g
381
395
 
@@ -406,7 +420,7 @@ You could also use a calculated column or view in your database.
406
420
  Given a field `foo` that was stored in the metadata, you can access it like so:
407
421
 
408
422
  ```ruby
409
- model_instance.last_transition.metadata["foo"]
423
+ model_instance.state_machine.last_transition.metadata["foo"]
410
424
  ```
411
425
 
412
426
  #### Events
@@ -425,6 +439,22 @@ class OrderStateMachine
425
439
  end
426
440
  ```
427
441
 
442
+ #### Deleting records.
443
+
444
+ If you need to delete the Parent model regularly you will need to change
445
+ either the association deletion behaviour or add a `DELETE CASCADE` condition
446
+ to foreign key in your database.
447
+
448
+ E.g
449
+ ```
450
+ has_many :order_transitions, autosave: false, dependent: :destroy
451
+ ```
452
+ or when migrating the transition model
453
+ ```
454
+ add_foreign_key :order_transitions, :orders, on_delete: :cascade
455
+ ```
456
+
457
+
428
458
  ## Testing Statesman Implementations
429
459
 
430
460
  This answer was abstracted from [this issue](https://github.com/gocardless/statesman/issues/77).
data/Rakefile CHANGED
@@ -1,13 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rspec/core/rake_task"
3
5
 
4
6
  RSpec::Core::RakeTask.new(:spec) do |task|
5
7
  task.rspec_opts = []
6
8
 
7
- if ENV["EXCLUDE_MONGOID"]
8
- task.rspec_opts += ["--tag ~mongo", "--exclude-pattern **/*mongo*"]
9
- end
10
-
11
9
  if ENV["CIRCLECI"]
12
10
  task.rspec_opts += ["--format RspecJunitFormatter",
13
11
  "--out /tmp/test-results/rspec.xml",
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rails/generators"
2
4
  require "generators/statesman/generator_helpers"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Statesman
2
4
  module GeneratorHelpers
3
5
  def class_name_option
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rails/generators"
2
4
  require "generators/statesman/generator_helpers"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "../exceptions"
2
4
 
3
5
  module Statesman
@@ -25,6 +27,7 @@ module Statesman
25
27
  elsif serialized && JSON_COLUMN_TYPES.include?(column_type)
26
28
  raise IncompatibleSerializationError, transition_class.name
27
29
  end
30
+
28
31
  @transition_class = transition_class
29
32
  @parent_model = parent_model
30
33
  @observer = observer
@@ -36,6 +39,7 @@ module Statesman
36
39
  create_transition(from.to_s, to.to_s, metadata)
37
40
  rescue ::ActiveRecord::RecordNotUnique => e
38
41
  raise TransitionConflictError, e.message if transition_conflict_error? e
42
+
39
43
  raise
40
44
  ensure
41
45
  @last_transition = nil
@@ -70,20 +74,28 @@ module Statesman
70
74
 
71
75
  transition = transitions_for_parent.build(transition_attributes)
72
76
 
73
- ::ActiveRecord::Base.transaction do
77
+ ::ActiveRecord::Base.transaction(requires_new: true) do
74
78
  @observer.execute(:before, from, to, transition)
75
79
  unset_old_most_recent
76
80
  transition.save!
77
81
  @last_transition = transition
78
82
  @observer.execute(:after, from, to, transition)
83
+ add_after_commit_callback(from, to, transition)
79
84
  end
80
- @observer.execute(:after_commit, from, to, transition)
81
85
 
82
86
  transition
83
87
  end
84
88
 
89
+ def add_after_commit_callback(from, to, transition)
90
+ ::ActiveRecord::Base.connection.add_transaction_record(
91
+ ActiveRecordAfterCommitWrap.new do
92
+ @observer.execute(:after_commit, from, to, transition)
93
+ end,
94
+ )
95
+ end
96
+
85
97
  def transitions_for_parent
86
- @parent_model.send(@association_name)
98
+ parent_model.send(@association_name)
87
99
  end
88
100
 
89
101
  def unset_old_most_recent
@@ -118,10 +130,46 @@ module Statesman
118
130
  end
119
131
 
120
132
  def transition_conflict_error?(err)
121
- err.message.include?(@transition_class.table_name) &&
133
+ return true if unique_indexes.any? { |i| err.message.include?(i.name) }
134
+
135
+ err.message.include?(transition_class.table_name) &&
122
136
  (err.message.include?("sort_key") || err.message.include?("most_recent"))
123
137
  end
124
138
 
139
+ def unique_indexes
140
+ ::ActiveRecord::Base.connection.
141
+ indexes(transition_class.table_name).
142
+ select do |index|
143
+ next unless index.unique
144
+
145
+ # We care about the columns used in the index, but not necessarily
146
+ # the order, which is why we sort both sides of the comparison here
147
+ index.columns.sort == [parent_join_foreign_key, "sort_key"].sort ||
148
+ index.columns.sort == [parent_join_foreign_key, "most_recent"].sort
149
+ end
150
+ end
151
+
152
+ def parent_join_foreign_key
153
+ association =
154
+ parent_model.class.
155
+ reflect_on_all_associations(:has_many).
156
+ find { |r| r.name.to_s == @association_name.to_s }
157
+
158
+ association_join_primary_key(association)
159
+ end
160
+
161
+ def association_join_primary_key(association)
162
+ if association.respond_to?(:join_primary_key)
163
+ association.join_primary_key
164
+ elsif association.method(:join_keys).arity.zero?
165
+ # Support for Rails 5.1
166
+ association.join_keys.key
167
+ else
168
+ # Support for Rails < 5.1
169
+ association.join_keys(transition_class).key
170
+ end
171
+ end
172
+
125
173
  def with_updated_timestamp(params)
126
174
  # TODO: Once we've set expectations that transition classes should conform to
127
175
  # the interface of Adapters::ActiveRecordTransition as a breaking change in the
@@ -130,8 +178,8 @@ module Statesman
130
178
  #
131
179
  # At the moment, most transition classes will include the module, but not all,
132
180
  # not least because it doesn't work with PostgreSQL JSON columns for metadata.
133
- column = if @transition_class.respond_to?(:updated_timestamp_column)
134
- @transition_class.updated_timestamp_column
181
+ column = if transition_class.respond_to?(:updated_timestamp_column)
182
+ transition_class.updated_timestamp_column
135
183
  else
136
184
  ActiveRecordTransition::DEFAULT_UPDATED_TIMESTAMP_COLUMN
137
185
  end
@@ -147,5 +195,39 @@ module Statesman
147
195
  params.merge(column => timestamp)
148
196
  end
149
197
  end
198
+
199
+ class ActiveRecordAfterCommitWrap
200
+ def initialize
201
+ @callback = Proc.new
202
+ @connection = ::ActiveRecord::Base.connection
203
+ end
204
+
205
+ def self.trigger_transactional_callbacks?
206
+ true
207
+ end
208
+
209
+ def trigger_transactional_callbacks?
210
+ true
211
+ end
212
+
213
+ # rubocop: disable Naming/PredicateName
214
+ def has_transactional_callbacks?
215
+ true
216
+ end
217
+ # rubocop: enable Naming/PredicateName
218
+
219
+ def committed!(*)
220
+ @callback.call
221
+ end
222
+
223
+ def before_committed!(*); end
224
+
225
+ def rolledback!(*); end
226
+
227
+ # Required for +transaction(requires_new: true)+
228
+ def add_to_transaction(*)
229
+ @connection.add_transaction_record(self)
230
+ end
231
+ end
150
232
  end
151
233
  end
@@ -1,51 +1,124 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Statesman
2
4
  module Adapters
3
5
  module ActiveRecordQueries
6
+ def self.check_missing_methods!(base)
7
+ missing_methods = %i[transition_class initial_state].
8
+ reject { |m| base.respond_to?(m) }
9
+ return if missing_methods.none?
10
+
11
+ raise NotImplementedError,
12
+ "#{missing_methods.join(', ')} method(s) should be defined on " \
13
+ "the model. Alternatively, use the new form of `include " \
14
+ "Statesman::Adapters::ActiveRecordQueries[" \
15
+ "transition_class: MyTransition, " \
16
+ "initial_state: :some_state]`"
17
+ end
18
+
4
19
  def self.included(base)
5
- base.extend(ClassMethods)
20
+ check_missing_methods!(base)
21
+
22
+ base.include(
23
+ ClassMethods.new(
24
+ transition_class: base.transition_class,
25
+ initial_state: base.initial_state,
26
+ most_recent_transition_alias: base.try(:most_recent_transition_alias),
27
+ transition_name: base.try(:transition_name),
28
+ ),
29
+ )
6
30
  end
7
31
 
8
- module ClassMethods
9
- def in_state(*states)
10
- states = states.flatten.map(&:to_s)
32
+ def self.[](**args)
33
+ ClassMethods.new(**args)
34
+ end
11
35
 
12
- joins(most_recent_transition_join).
13
- where(states_where(most_recent_transition_alias, states), states)
36
+ class ClassMethods < Module
37
+ def initialize(**args)
38
+ @args = args
14
39
  end
15
40
 
16
- def not_in_state(*states)
17
- states = states.flatten.map(&:to_s)
41
+ def included(base)
42
+ ensure_inheritance(base)
18
43
 
19
- joins(most_recent_transition_join).
20
- where("NOT (#{states_where(most_recent_transition_alias, states)})",
21
- states)
22
- end
44
+ query_builder = QueryBuilder.new(base, **@args)
23
45
 
24
- def most_recent_transition_join
25
- "LEFT OUTER JOIN #{model_table} AS #{most_recent_transition_alias}
26
- ON #{table_name}.id =
27
- #{most_recent_transition_alias}.#{model_foreign_key}
28
- AND #{most_recent_transition_alias}.most_recent = #{db_true}"
46
+ base.define_singleton_method(:most_recent_transition_join) do
47
+ query_builder.most_recent_transition_join
48
+ end
49
+
50
+ define_in_state(base, query_builder)
51
+ define_not_in_state(base, query_builder)
29
52
  end
30
53
 
31
54
  private
32
55
 
33
- def transition_class
34
- raise NotImplementedError, "A transition_class method should be " \
35
- "defined on the model"
56
+ def ensure_inheritance(base)
57
+ klass = self
58
+ existing_inherited = base.method(:inherited)
59
+ base.define_singleton_method(:inherited) do |subclass|
60
+ existing_inherited.call(subclass)
61
+ subclass.send(:include, klass)
62
+ end
63
+ end
64
+
65
+ def define_in_state(base, query_builder)
66
+ base.define_singleton_method(:in_state) do |*states|
67
+ states = states.flatten
68
+
69
+ joins(most_recent_transition_join).
70
+ where(query_builder.states_where(states), states)
71
+ end
72
+ end
73
+
74
+ def define_not_in_state(base, query_builder)
75
+ base.define_singleton_method(:not_in_state) do |*states|
76
+ states = states.flatten
77
+
78
+ joins(most_recent_transition_join).
79
+ where("NOT (#{query_builder.states_where(states)})", states)
80
+ end
81
+ end
82
+ end
83
+
84
+ class QueryBuilder
85
+ def initialize(model, transition_class:, initial_state:,
86
+ most_recent_transition_alias: nil,
87
+ transition_name: nil)
88
+ @model = model
89
+ @transition_class = transition_class
90
+ @initial_state = initial_state
91
+ @most_recent_transition_alias = most_recent_transition_alias
92
+ @transition_name = transition_name
36
93
  end
37
94
 
38
- def initial_state
39
- raise NotImplementedError, "An initial_state method should be " \
40
- "defined on the model"
95
+ def states_where(states)
96
+ if initial_state.to_s.in?(states.map(&:to_s))
97
+ "#{most_recent_transition_alias}.to_state IN (?) OR " \
98
+ "#{most_recent_transition_alias}.to_state IS NULL"
99
+ else
100
+ "#{most_recent_transition_alias}.to_state IN (?) AND " \
101
+ "#{most_recent_transition_alias}.to_state IS NOT NULL"
102
+ end
103
+ end
104
+
105
+ def most_recent_transition_join
106
+ "LEFT OUTER JOIN #{model_table} AS #{most_recent_transition_alias} " \
107
+ "ON #{model.table_name}.id = " \
108
+ "#{most_recent_transition_alias}.#{model_foreign_key} " \
109
+ "AND #{most_recent_transition_alias}.most_recent = #{db_true}"
41
110
  end
42
111
 
112
+ private
113
+
114
+ attr_reader :model, :transition_class, :initial_state
115
+
43
116
  def transition_name
44
- transition_class.table_name.to_sym
117
+ @transition_name || transition_class.table_name.to_sym
45
118
  end
46
119
 
47
120
  def transition_reflection
48
- reflect_on_all_associations(:has_many).each do |value|
121
+ model.reflect_on_all_associations(:has_many).each do |value|
49
122
  return value if value.klass == transition_class
50
123
  end
51
124
 
@@ -62,18 +135,9 @@ module Statesman
62
135
  transition_reflection.table_name
63
136
  end
64
137
 
65
- def states_where(temporary_table_name, states)
66
- if initial_state.to_s.in?(states.map(&:to_s))
67
- "#{temporary_table_name}.to_state IN (?) OR " \
68
- "#{temporary_table_name}.to_state IS NULL"
69
- else
70
- "#{temporary_table_name}.to_state IN (?) AND " \
71
- "#{temporary_table_name}.to_state IS NOT NULL"
72
- end
73
- end
74
-
75
138
  def most_recent_transition_alias
76
- "most_recent_#{transition_name.to_s.singularize}"
139
+ @most_recent_transition_alias ||
140
+ "most_recent_#{transition_name.to_s.singularize}"
77
141
  end
78
142
 
79
143
  def db_true
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
 
3
5
  module Statesman
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
 
3
5
  module Statesman
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Statesman
2
4
  module Adapters
3
5
  class MemoryTransition
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "exceptions"
2
4
 
3
5
  module Statesman
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "json"
2
4
  require_relative "exceptions"
3
5
 
@@ -1,9 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Statesman
2
4
  class InvalidStateError < StandardError; end
3
5
  class InvalidTransitionError < StandardError; end
4
6
  class InvalidCallbackError < StandardError; end
5
- class GuardFailedError < StandardError; end
6
- class TransitionFailedError < StandardError; end
7
+
8
+ class TransitionFailedError < StandardError
9
+ def initialize(from, to)
10
+ @from = from
11
+ @to = to
12
+ end
13
+
14
+ attr_reader :from, :to
15
+
16
+ def message
17
+ "Cannot transition from '#{from}' to '#{to}'"
18
+ end
19
+ end
20
+
21
+ class GuardFailedError < StandardError
22
+ def initialize(from, to)
23
+ @from = from
24
+ @to = to
25
+ end
26
+
27
+ attr_reader :from, :to
28
+
29
+ def message
30
+ "Guard on transition from: '#{from}' to '#{to}' returned false"
31
+ end
32
+ end
33
+
7
34
  class TransitionConflictError < StandardError; end
8
35
  class MissingTransitionAssociation < StandardError; end
9
36
 
@@ -1,13 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "callback"
2
4
  require_relative "exceptions"
3
5
 
4
6
  module Statesman
5
7
  class Guard < Callback
6
8
  def call(*args)
7
- unless super(*args)
8
- raise GuardFailedError,
9
- "Guard on transition from: '#{from}' to '#{to}' returned false"
10
- end
9
+ raise GuardFailedError.new(from, to) unless super(*args)
11
10
  end
12
11
  end
13
12
  end