statesman 3.5.0 → 6.0.0

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