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.
- checksums.yaml +5 -5
- data/.circleci/config.yml +45 -225
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +26 -6
- data/CHANGELOG.md +69 -0
- data/Gemfile +9 -3
- data/Guardfile +2 -0
- data/README.md +77 -47
- data/Rakefile +2 -4
- data/lib/generators/statesman/active_record_transition_generator.rb +2 -0
- data/lib/generators/statesman/generator_helpers.rb +2 -0
- data/lib/generators/statesman/migration_generator.rb +2 -0
- data/lib/statesman/adapters/active_record.rb +88 -6
- data/lib/statesman/adapters/active_record_queries.rb +100 -36
- data/lib/statesman/adapters/active_record_transition.rb +2 -0
- data/lib/statesman/adapters/memory.rb +2 -0
- data/lib/statesman/adapters/memory_transition.rb +2 -0
- data/lib/statesman/callback.rb +2 -0
- data/lib/statesman/config.rb +2 -0
- data/lib/statesman/exceptions.rb +29 -2
- data/lib/statesman/guard.rb +3 -4
- data/lib/statesman/machine.rb +29 -7
- data/lib/statesman/railtie.rb +2 -0
- data/lib/statesman/utils.rb +2 -0
- data/lib/statesman/version.rb +3 -1
- data/lib/statesman.rb +2 -3
- data/lib/tasks/statesman.rake +3 -1
- data/spec/fixtures/add_constraints_to_most_recent_for_bacon_transitions_with_partial_index.rb +2 -0
- data/spec/fixtures/add_constraints_to_most_recent_for_bacon_transitions_without_partial_index.rb +2 -0
- data/spec/fixtures/add_most_recent_to_bacon_transitions.rb +2 -0
- data/spec/generators/statesman/active_record_transition_generator_spec.rb +2 -0
- data/spec/generators/statesman/migration_generator_spec.rb +2 -0
- data/spec/spec_helper.rb +3 -30
- data/spec/statesman/adapters/active_record_queries_spec.rb +165 -91
- data/spec/statesman/adapters/active_record_spec.rb +4 -0
- data/spec/statesman/adapters/active_record_transition_spec.rb +2 -0
- data/spec/statesman/adapters/memory_spec.rb +2 -0
- data/spec/statesman/adapters/memory_transition_spec.rb +2 -0
- data/spec/statesman/adapters/shared_examples.rb +2 -0
- data/spec/statesman/callback_spec.rb +2 -0
- data/spec/statesman/config_spec.rb +2 -0
- data/spec/statesman/guard_spec.rb +2 -0
- data/spec/statesman/machine_spec.rb +79 -4
- data/spec/statesman/utils_spec.rb +2 -0
- data/spec/support/active_record.rb +9 -12
- data/spec/support/generators_shared_examples.rb +2 -0
- data/statesman.gemspec +5 -3
- metadata +17 -22
- data/lib/generators/statesman/mongoid_transition_generator.rb +0 -25
- data/lib/generators/statesman/templates/mongoid_transition_model.rb.erb +0 -14
- data/lib/statesman/adapters/mongoid.rb +0 -66
- data/lib/statesman/adapters/mongoid_transition.rb +0 -10
- data/spec/generators/statesman/mongoid_transition_generator_spec.rb +0 -23
- data/spec/statesman/adapters/mongoid_spec.rb +0 -86
- data/spec/support/mongoid.rb +0 -28
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
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', '~>
|
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
|
-
|
168
|
-
|
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
|
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
|
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
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
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
|
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
|
-
|
357
|
-
:
|
358
|
-
|
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
|
-
|
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_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
|
-
|
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?(
|
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
|
134
|
-
|
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
|
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
|
-
|
9
|
-
|
10
|
-
|
32
|
+
def self.[](**args)
|
33
|
+
ClassMethods.new(**args)
|
34
|
+
end
|
11
35
|
|
12
|
-
|
13
|
-
|
36
|
+
class ClassMethods < Module
|
37
|
+
def initialize(**args)
|
38
|
+
@args = args
|
14
39
|
end
|
15
40
|
|
16
|
-
def
|
17
|
-
|
41
|
+
def included(base)
|
42
|
+
ensure_inheritance(base)
|
18
43
|
|
19
|
-
|
20
|
-
where("NOT (#{states_where(most_recent_transition_alias, states)})",
|
21
|
-
states)
|
22
|
-
end
|
44
|
+
query_builder = QueryBuilder.new(base, **@args)
|
23
45
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
34
|
-
|
35
|
-
|
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
|
39
|
-
|
40
|
-
|
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
|
-
|
139
|
+
@most_recent_transition_alias ||
|
140
|
+
"most_recent_#{transition_name.to_s.singularize}"
|
77
141
|
end
|
78
142
|
|
79
143
|
def db_true
|
data/lib/statesman/callback.rb
CHANGED
data/lib/statesman/config.rb
CHANGED
data/lib/statesman/exceptions.rb
CHANGED
@@ -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
|
-
|
6
|
-
class TransitionFailedError < StandardError
|
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
|
|
data/lib/statesman/guard.rb
CHANGED
@@ -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
|