statesman 7.4.0 → 10.2.3
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 +4 -4
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/tests.yml +106 -0
- data/.gitignore +68 -15
- data/.rubocop.yml +14 -1
- data/.rubocop_todo.yml +37 -28
- data/.ruby-version +1 -0
- data/CHANGELOG.md +87 -6
- data/CONTRIBUTING.md +18 -0
- data/Gemfile +3 -5
- data/README.md +147 -5
- data/lib/generators/statesman/active_record_transition_generator.rb +1 -1
- data/lib/generators/statesman/generator_helpers.rb +11 -3
- data/lib/statesman/adapters/active_record.rb +61 -25
- data/lib/statesman/adapters/active_record_queries.rb +17 -5
- data/lib/statesman/adapters/memory.rb +5 -1
- data/lib/statesman/adapters/type_safe_active_record_queries.rb +21 -0
- data/lib/statesman/exceptions.rb +13 -7
- data/lib/statesman/guard.rb +1 -1
- data/lib/statesman/machine.rb +60 -0
- data/lib/statesman/version.rb +1 -1
- data/lib/statesman.rb +2 -0
- data/lib/tasks/statesman.rake +3 -3
- data/spec/spec_helper.rb +11 -0
- data/spec/statesman/adapters/active_record_queries_spec.rb +33 -9
- data/spec/statesman/adapters/active_record_spec.rb +125 -19
- data/spec/statesman/adapters/shared_examples.rb +3 -2
- data/spec/statesman/adapters/type_safe_active_record_queries_spec.rb +208 -0
- data/spec/statesman/exceptions_spec.rb +16 -1
- data/spec/statesman/machine_spec.rb +181 -13
- data/spec/support/active_record.rb +105 -15
- data/statesman.gemspec +8 -9
- metadata +28 -57
- data/.circleci/config.yml +0 -187
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
<p align="center"><img src="
|
1
|
+
<p align="center"><img src="https://user-images.githubusercontent.com/110275/106792848-96e4ee80-664e-11eb-8fd1-16ff24b41eb2.png" alt="Statesman" width="512"></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', '~> 10.0.0'
|
34
34
|
```
|
35
35
|
|
36
36
|
## Usage
|
@@ -109,11 +109,59 @@ Order.first.state_machine.allowed_transitions # => ["checking_out", "cancelled"]
|
|
109
109
|
Order.first.state_machine.can_transition_to?(:cancelled) # => true/false
|
110
110
|
Order.first.state_machine.transition_to(:cancelled, optional: :metadata) # => true/false
|
111
111
|
Order.first.state_machine.transition_to!(:cancelled) # => true/exception
|
112
|
+
Order.first.state_machine.last_transition # => transition model or nil
|
113
|
+
Order.first.state_machine.last_transition_to(:pending) # => transition model or nil
|
112
114
|
|
113
115
|
Order.in_state(:cancelled) # => [#<Order id: "123">]
|
114
116
|
Order.not_in_state(:checking_out) # => [#<Order id: "123">]
|
115
117
|
```
|
116
118
|
|
119
|
+
If you'd like, you can also define a template for a generic state machine, then alter classes which extend it as required:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
module Template
|
123
|
+
def define_states
|
124
|
+
state :a, initial: true
|
125
|
+
state :b
|
126
|
+
state :c
|
127
|
+
end
|
128
|
+
|
129
|
+
def define_transitions
|
130
|
+
transition from: :a, to: :b
|
131
|
+
transition from: :b, to: :c
|
132
|
+
transition from: :c, to: :a
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class Circular
|
137
|
+
include Statesman::Machine
|
138
|
+
extend Template
|
139
|
+
|
140
|
+
define_states
|
141
|
+
define_transitions
|
142
|
+
end
|
143
|
+
|
144
|
+
class Linear
|
145
|
+
include Statesman::Machine
|
146
|
+
extend Template
|
147
|
+
|
148
|
+
define_states
|
149
|
+
define_transitions
|
150
|
+
|
151
|
+
remove_transitions from: :c, to: :a
|
152
|
+
end
|
153
|
+
|
154
|
+
class Shorter
|
155
|
+
include Statesman::Machine
|
156
|
+
extend Template
|
157
|
+
|
158
|
+
define_states
|
159
|
+
define_transitions
|
160
|
+
|
161
|
+
remove_state :c
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
117
165
|
## Persistence
|
118
166
|
|
119
167
|
By default Statesman stores transition history in memory only. It can be
|
@@ -159,7 +207,8 @@ class Order < ActiveRecord::Base
|
|
159
207
|
|
160
208
|
# Optionally delegate some methods
|
161
209
|
|
162
|
-
delegate :can_transition_to?,
|
210
|
+
delegate :can_transition_to?,
|
211
|
+
:current_state, :history, :last_transition, :last_transition_to,
|
163
212
|
:transition_to!, :transition_to, :in_state?, to: :state_machine
|
164
213
|
end
|
165
214
|
```
|
@@ -322,6 +371,10 @@ Machine.successors
|
|
322
371
|
#### `Machine#current_state`
|
323
372
|
Returns the current state based on existing transition objects.
|
324
373
|
|
374
|
+
Takes an optional keyword argument to force a reload of data from the
|
375
|
+
database.
|
376
|
+
e.g `current_state(force_reload: true)`
|
377
|
+
|
325
378
|
#### `Machine#in_state?(:state_1, :state_2, ...)`
|
326
379
|
Returns true if the machine is in any of the given states.
|
327
380
|
|
@@ -331,6 +384,9 @@ Returns a sorted array of all transition objects.
|
|
331
384
|
#### `Machine#last_transition`
|
332
385
|
Returns the most recent transition object.
|
333
386
|
|
387
|
+
#### `Machine#last_transition_to(:state)`
|
388
|
+
Returns the most recent transition object to a given state.
|
389
|
+
|
334
390
|
#### `Machine#allowed_transitions`
|
335
391
|
Returns an array of states you can `transition_to` from current state.
|
336
392
|
|
@@ -347,6 +403,66 @@ Transition to the passed state, returning `true` on success. Swallows all
|
|
347
403
|
Statesman exceptions and returns false on failure. (NB. if your guard or
|
348
404
|
callback code throws an exception, it will not be caught.)
|
349
405
|
|
406
|
+
|
407
|
+
## Errors
|
408
|
+
|
409
|
+
### Initialization errors
|
410
|
+
These errors are raised when the Machine and/or Model is initialized. A simple spec like
|
411
|
+
```ruby
|
412
|
+
expect { OrderStateMachine.new(Order.new, transition_class: OrderTransition) }.to_not raise_error
|
413
|
+
```
|
414
|
+
will expose these errors as part of your test suite
|
415
|
+
|
416
|
+
#### InvalidStateError
|
417
|
+
Raised if:
|
418
|
+
* Attempting to define a transition without a `to` state.
|
419
|
+
* Attempting to define a transition with a non-existent state.
|
420
|
+
* Attempting to define multiple states as `initial`.
|
421
|
+
|
422
|
+
#### InvalidTransitionError
|
423
|
+
Raised if:
|
424
|
+
* Attempting to define a callback `from` a state that has no valid transitions (A terminal state).
|
425
|
+
* Attempting to define a callback `to` the `initial` state if that state has no transitions to it.
|
426
|
+
* Attempting to define a callback with `from` and `to` where any of the pairs have no transition between them.
|
427
|
+
|
428
|
+
#### InvalidCallbackError
|
429
|
+
Raised if:
|
430
|
+
* Attempting to define a callback without a block.
|
431
|
+
|
432
|
+
#### UnserializedMetadataError
|
433
|
+
Raised if:
|
434
|
+
* ActiveRecord is configured to not serialize the `metadata` attribute into
|
435
|
+
to Database column backing it. See the `Using PostgreSQL JSON column` section.
|
436
|
+
|
437
|
+
#### IncompatibleSerializationError
|
438
|
+
Raised if:
|
439
|
+
* There is a mismatch between the column type of the `metadata` in the
|
440
|
+
Database and the model. See the `Using PostgreSQL JSON column` section.
|
441
|
+
|
442
|
+
#### MissingTransitionAssociation
|
443
|
+
Raised if:
|
444
|
+
* The model that `Statesman::Adapters::ActiveRecordQueries` is included in
|
445
|
+
does not have a `has_many` association to the `transition_class`.
|
446
|
+
|
447
|
+
### Runtime errors
|
448
|
+
These errors are raised by `transition_to!`. Using `transition_to` will
|
449
|
+
supress `GuardFailedError` and `TransitionFailedError` and return `false` instead.
|
450
|
+
|
451
|
+
#### GuardFailedError
|
452
|
+
Raised if:
|
453
|
+
* A guard callback between `from` and `to` state returned a falsey value.
|
454
|
+
|
455
|
+
#### TransitionFailedError
|
456
|
+
Raised if:
|
457
|
+
* A transition is attempted but `current_state -> new_state` is not a valid pair.
|
458
|
+
|
459
|
+
#### TransitionConflictError
|
460
|
+
Raised if:
|
461
|
+
* A database conflict affecting the `sort_key` or `most_recent` columns occurs
|
462
|
+
when attempting a transition.
|
463
|
+
Retried automatically if it occurs wrapped in `retry_conflicts`.
|
464
|
+
|
465
|
+
|
350
466
|
## Model scopes
|
351
467
|
|
352
468
|
A mixin is provided for the ActiveRecord adapter which adds scopes to easily
|
@@ -404,10 +520,12 @@ Model.in_state(:state_1).or(
|
|
404
520
|
#### Storing the state on the model object
|
405
521
|
|
406
522
|
If you wish to store the model state on the model directly, you can keep it up
|
407
|
-
to date using an `after_transition` hook
|
523
|
+
to date using an `after_transition` hook.
|
524
|
+
Combine it with the `after_commit` option to ensure the model state will only be
|
525
|
+
saved once the transition has made it irreversibly to the database:
|
408
526
|
|
409
527
|
```ruby
|
410
|
-
after_transition do |model, transition|
|
528
|
+
after_transition(after_commit: true) do |model, transition|
|
411
529
|
model.state = transition.to_state
|
412
530
|
model.save!
|
413
531
|
end
|
@@ -493,6 +611,30 @@ describe "some callback" do
|
|
493
611
|
end
|
494
612
|
```
|
495
613
|
|
614
|
+
## Compatibility with type checkers
|
615
|
+
|
616
|
+
Including ActiveRecordQueries to your model can cause issues with type checkers
|
617
|
+
such as Sorbet, this is because this technically is using a dynamic include,
|
618
|
+
which is not supported by Sorbet.
|
619
|
+
|
620
|
+
To avoid these issues you can instead include the TypeSafeActiveRecordQueries
|
621
|
+
module and pass in configuration.
|
622
|
+
|
623
|
+
```ruby
|
624
|
+
class Order < ActiveRecord::Base
|
625
|
+
has_many :order_transitions, autosave: false
|
626
|
+
|
627
|
+
include Statesman::Adapters::TypeSafeActiveRecordQueries
|
628
|
+
|
629
|
+
configure_state_machine transition_class: OrderTransition,
|
630
|
+
initial_state: :pending
|
631
|
+
|
632
|
+
def state_machine
|
633
|
+
@state_machine ||= OrderStateMachine.new(self, transition_class: OrderTransition)
|
634
|
+
end
|
635
|
+
end
|
636
|
+
```
|
637
|
+
|
496
638
|
# Third-party extensions
|
497
639
|
|
498
640
|
[statesman-sequel](https://github.com/badosu/statesman-sequel) - An adapter to make Statesman work with [Sequel](https://github.com/jeremyevans/sequel)
|
@@ -7,7 +7,7 @@ module Statesman
|
|
7
7
|
class ActiveRecordTransitionGenerator < Rails::Generators::Base
|
8
8
|
include Statesman::GeneratorHelpers
|
9
9
|
|
10
|
-
desc "Create an ActiveRecord-based transition model"\
|
10
|
+
desc "Create an ActiveRecord-based transition model" \
|
11
11
|
"with the required attributes"
|
12
12
|
|
13
13
|
argument :parent, type: :string, desc: "Your parent model name"
|
@@ -11,7 +11,7 @@ module Statesman
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def migration_class_name
|
14
|
-
klass.gsub(
|
14
|
+
klass.gsub("::", "").pluralize
|
15
15
|
end
|
16
16
|
|
17
17
|
def next_migration_number
|
@@ -39,8 +39,16 @@ module Statesman
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def mysql?
|
42
|
-
|
43
|
-
|
42
|
+
configuration.try(:[], "adapter").try(:match, /mysql/)
|
43
|
+
end
|
44
|
+
|
45
|
+
# [] is deprecated and will be removed in 6.2
|
46
|
+
def configuration
|
47
|
+
if ActiveRecord::Base.configurations.respond_to?(:configs_for)
|
48
|
+
ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).first
|
49
|
+
else
|
50
|
+
ActiveRecord::Base.configurations[Rails.env]
|
51
|
+
end
|
44
52
|
end
|
45
53
|
|
46
54
|
def database_supports_partial_indexes?
|
@@ -42,11 +42,17 @@ module Statesman
|
|
42
42
|
def create(from, to, metadata = {})
|
43
43
|
create_transition(from.to_s, to.to_s, metadata)
|
44
44
|
rescue ::ActiveRecord::RecordNotUnique => e
|
45
|
-
|
45
|
+
if transition_conflict_error? e
|
46
|
+
# The history has the invalid transition on the end of it, which means
|
47
|
+
# `current_state` would then be incorrect. We force a reload of the history to
|
48
|
+
# avoid this.
|
49
|
+
transitions_for_parent.reload
|
50
|
+
raise TransitionConflictError, e.message
|
51
|
+
end
|
46
52
|
|
47
53
|
raise
|
48
54
|
ensure
|
49
|
-
|
55
|
+
reset
|
50
56
|
end
|
51
57
|
|
52
58
|
def history(force_reload: false)
|
@@ -62,14 +68,21 @@ module Statesman
|
|
62
68
|
def last(force_reload: false)
|
63
69
|
if force_reload
|
64
70
|
@last_transition = history(force_reload: true).last
|
71
|
+
elsif instance_variable_defined?(:@last_transition)
|
72
|
+
@last_transition
|
65
73
|
else
|
66
|
-
@last_transition
|
74
|
+
@last_transition = history.last
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def reset
|
79
|
+
if instance_variable_defined?(:@last_transition)
|
80
|
+
remove_instance_variable(:@last_transition)
|
67
81
|
end
|
68
82
|
end
|
69
83
|
|
70
84
|
private
|
71
85
|
|
72
|
-
# rubocop:disable Metrics/MethodLength
|
73
86
|
def create_transition(from, to, metadata)
|
74
87
|
transition = transitions_for_parent.build(
|
75
88
|
default_transition_attributes(to, metadata),
|
@@ -106,7 +119,6 @@ module Statesman
|
|
106
119
|
|
107
120
|
transition
|
108
121
|
end
|
109
|
-
# rubocop:enable Metrics/MethodLength
|
110
122
|
|
111
123
|
def default_transition_attributes(to, metadata)
|
112
124
|
{
|
@@ -147,13 +159,24 @@ module Statesman
|
|
147
159
|
|
148
160
|
def most_recent_transitions(most_recent_id = nil)
|
149
161
|
if most_recent_id
|
150
|
-
|
162
|
+
concrete_transitions_of_parent.and(
|
151
163
|
transition_table[:id].eq(most_recent_id).or(
|
152
164
|
transition_table[:most_recent].eq(true),
|
153
165
|
),
|
154
166
|
)
|
155
167
|
else
|
156
|
-
|
168
|
+
concrete_transitions_of_parent.and(transition_table[:most_recent].eq(true))
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def concrete_transitions_of_parent
|
173
|
+
if transition_sti?
|
174
|
+
transitions_of_parent.and(
|
175
|
+
transition_table[transition_class.inheritance_column].
|
176
|
+
eq(transition_class.name),
|
177
|
+
)
|
178
|
+
else
|
179
|
+
transitions_of_parent
|
157
180
|
end
|
158
181
|
end
|
159
182
|
|
@@ -219,7 +242,7 @@ module Statesman
|
|
219
242
|
end
|
220
243
|
|
221
244
|
def next_sort_key
|
222
|
-
(last && last.sort_key + 10) || 10
|
245
|
+
(last && (last.sort_key + 10)) || 10
|
223
246
|
end
|
224
247
|
|
225
248
|
def serialized?(transition_class)
|
@@ -252,13 +275,18 @@ module Statesman
|
|
252
275
|
end
|
253
276
|
end
|
254
277
|
|
255
|
-
def
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
278
|
+
def transition_sti?
|
279
|
+
transition_class.column_names.include?(transition_class.inheritance_column)
|
280
|
+
end
|
281
|
+
|
282
|
+
def parent_association
|
283
|
+
parent_model.class.
|
284
|
+
reflect_on_all_associations(:has_many).
|
285
|
+
find { |r| r.name.to_s == @association_name.to_s }
|
286
|
+
end
|
260
287
|
|
261
|
-
|
288
|
+
def parent_join_foreign_key
|
289
|
+
association_join_primary_key(parent_association)
|
262
290
|
end
|
263
291
|
|
264
292
|
def association_join_primary_key(association)
|
@@ -292,34 +320,42 @@ module Statesman
|
|
292
320
|
return nil if column.nil?
|
293
321
|
|
294
322
|
[
|
295
|
-
column,
|
323
|
+
column, default_timezone == :utc ? Time.now.utc : Time.now
|
296
324
|
]
|
297
325
|
end
|
298
326
|
|
327
|
+
def default_timezone
|
328
|
+
# Rails 7 deprecates ActiveRecord::Base.default_timezone
|
329
|
+
# in favour of ActiveRecord.default_timezone
|
330
|
+
if ::ActiveRecord.respond_to?(:default_timezone)
|
331
|
+
return ::ActiveRecord.default_timezone
|
332
|
+
end
|
333
|
+
|
334
|
+
::ActiveRecord::Base.default_timezone
|
335
|
+
end
|
336
|
+
|
299
337
|
def mysql_gaplock_protection?
|
300
338
|
Statesman.mysql_gaplock_protection?
|
301
339
|
end
|
302
340
|
|
303
341
|
def db_true
|
304
|
-
|
305
|
-
true,
|
306
|
-
transition_class.columns_hash["most_recent"],
|
307
|
-
)
|
308
|
-
::ActiveRecord::Base.connection.quote(value)
|
342
|
+
::ActiveRecord::Base.connection.quote(type_cast(true))
|
309
343
|
end
|
310
344
|
|
311
345
|
def db_false
|
312
|
-
|
313
|
-
false,
|
314
|
-
transition_class.columns_hash["most_recent"],
|
315
|
-
)
|
316
|
-
::ActiveRecord::Base.connection.quote(value)
|
346
|
+
::ActiveRecord::Base.connection.quote(type_cast(false))
|
317
347
|
end
|
318
348
|
|
319
349
|
def db_null
|
320
350
|
Arel::Nodes::SqlLiteral.new("NULL")
|
321
351
|
end
|
322
352
|
|
353
|
+
# Type casting against a column is deprecated and will be removed in Rails 6.2.
|
354
|
+
# See https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11
|
355
|
+
def type_cast(value)
|
356
|
+
::ActiveRecord::Base.connection.type_cast(value)
|
357
|
+
end
|
358
|
+
|
323
359
|
# Check whether the `most_recent` column allows null values. If it doesn't, set old
|
324
360
|
# records to `false`, otherwise, set them to `NULL`.
|
325
361
|
#
|
@@ -49,6 +49,14 @@ module Statesman
|
|
49
49
|
|
50
50
|
define_in_state(base, query_builder)
|
51
51
|
define_not_in_state(base, query_builder)
|
52
|
+
|
53
|
+
define_method(:reload) do |*a|
|
54
|
+
instance = super(*a)
|
55
|
+
if instance.respond_to?(:state_machine, true)
|
56
|
+
instance.send(:state_machine).reset
|
57
|
+
end
|
58
|
+
instance
|
59
|
+
end
|
52
60
|
end
|
53
61
|
|
54
62
|
private
|
@@ -95,18 +103,18 @@ module Statesman
|
|
95
103
|
def states_where(states)
|
96
104
|
if initial_state.to_s.in?(states.map(&:to_s))
|
97
105
|
"#{most_recent_transition_alias}.to_state IN (?) OR " \
|
98
|
-
|
106
|
+
"#{most_recent_transition_alias}.to_state IS NULL"
|
99
107
|
else
|
100
108
|
"#{most_recent_transition_alias}.to_state IN (?) AND " \
|
101
|
-
|
109
|
+
"#{most_recent_transition_alias}.to_state IS NOT NULL"
|
102
110
|
end
|
103
111
|
end
|
104
112
|
|
105
113
|
def most_recent_transition_join
|
106
114
|
"LEFT OUTER JOIN #{model_table} AS #{most_recent_transition_alias} " \
|
107
|
-
|
108
|
-
|
109
|
-
|
115
|
+
"ON #{model.table_name}.#{model_primary_key} = " \
|
116
|
+
"#{most_recent_transition_alias}.#{model_foreign_key} " \
|
117
|
+
"AND #{most_recent_transition_alias}.most_recent = #{db_true}"
|
110
118
|
end
|
111
119
|
|
112
120
|
private
|
@@ -127,6 +135,10 @@ module Statesman
|
|
127
135
|
"and #{transition_class}."
|
128
136
|
end
|
129
137
|
|
138
|
+
def model_primary_key
|
139
|
+
transition_reflection.active_record_primary_key
|
140
|
+
end
|
141
|
+
|
130
142
|
def model_foreign_key
|
131
143
|
transition_reflection.foreign_key
|
132
144
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Statesman
|
4
|
+
module Adapters
|
5
|
+
module TypeSafeActiveRecordQueries
|
6
|
+
def configure_state_machine(args = {})
|
7
|
+
transition_class = args.fetch(:transition_class)
|
8
|
+
initial_state = args.fetch(:initial_state)
|
9
|
+
|
10
|
+
include(
|
11
|
+
ActiveRecordQueries::ClassMethods.new(
|
12
|
+
transition_class: transition_class,
|
13
|
+
initial_state: initial_state,
|
14
|
+
most_recent_transition_alias: try(:most_recent_transition_alias),
|
15
|
+
transition_name: try(:transition_name),
|
16
|
+
),
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/statesman/exceptions.rb
CHANGED
@@ -2,9 +2,13 @@
|
|
2
2
|
|
3
3
|
module Statesman
|
4
4
|
class InvalidStateError < StandardError; end
|
5
|
+
|
5
6
|
class InvalidTransitionError < StandardError; end
|
7
|
+
|
6
8
|
class InvalidCallbackError < StandardError; end
|
9
|
+
|
7
10
|
class TransitionConflictError < StandardError; end
|
11
|
+
|
8
12
|
class MissingTransitionAssociation < StandardError; end
|
9
13
|
|
10
14
|
class TransitionFailedError < StandardError
|
@@ -24,13 +28,15 @@ module Statesman
|
|
24
28
|
end
|
25
29
|
|
26
30
|
class GuardFailedError < StandardError
|
27
|
-
def initialize(from, to)
|
31
|
+
def initialize(from, to, callback)
|
28
32
|
@from = from
|
29
33
|
@to = to
|
34
|
+
@callback = callback
|
30
35
|
super(_message)
|
36
|
+
set_backtrace(callback.source_location.join(":")) if callback&.source_location
|
31
37
|
end
|
32
38
|
|
33
|
-
attr_reader :from, :to
|
39
|
+
attr_reader :from, :to, :callback
|
34
40
|
|
35
41
|
private
|
36
42
|
|
@@ -48,8 +54,8 @@ module Statesman
|
|
48
54
|
|
49
55
|
def _message(transition_class_name)
|
50
56
|
"#{transition_class_name}#metadata is not serialized. If you " \
|
51
|
-
|
52
|
-
|
57
|
+
"are using a non-json column type, you should `include " \
|
58
|
+
"Statesman::Adapters::ActiveRecordTransition`"
|
53
59
|
end
|
54
60
|
end
|
55
61
|
|
@@ -62,9 +68,9 @@ module Statesman
|
|
62
68
|
|
63
69
|
def _message(transition_class_name)
|
64
70
|
"#{transition_class_name}#metadata column type cannot be json " \
|
65
|
-
|
66
|
-
|
67
|
-
|
71
|
+
"and serialized simultaneously. If you are using a json " \
|
72
|
+
"column type, it is not necessary to `include " \
|
73
|
+
"Statesman::Adapters::ActiveRecordTransition`"
|
68
74
|
end
|
69
75
|
end
|
70
76
|
end
|
data/lib/statesman/guard.rb
CHANGED
data/lib/statesman/machine.rb
CHANGED
@@ -42,6 +42,17 @@ module Statesman
|
|
42
42
|
states << name
|
43
43
|
end
|
44
44
|
|
45
|
+
def remove_state(state_name)
|
46
|
+
state_name = state_name.to_s
|
47
|
+
|
48
|
+
remove_transitions(from: state_name)
|
49
|
+
remove_transitions(to: state_name)
|
50
|
+
remove_callbacks(from: state_name)
|
51
|
+
remove_callbacks(to: state_name)
|
52
|
+
|
53
|
+
@states.delete(state_name.to_s)
|
54
|
+
end
|
55
|
+
|
45
56
|
def successors
|
46
57
|
@successors ||= {}
|
47
58
|
end
|
@@ -70,6 +81,20 @@ module Statesman
|
|
70
81
|
successors[from] += to
|
71
82
|
end
|
72
83
|
|
84
|
+
def remove_transitions(from: nil, to: nil)
|
85
|
+
raise ArgumentError, "Both from and to can't be nil!" if from.nil? && to.nil?
|
86
|
+
return if successors.nil?
|
87
|
+
|
88
|
+
if from.present?
|
89
|
+
@successors[from.to_s].delete(to.to_s) if to.present?
|
90
|
+
@successors.delete(from.to_s) if to.nil? || successors[from.to_s].empty?
|
91
|
+
elsif to.present?
|
92
|
+
@successors.
|
93
|
+
transform_values! { |to_states| to_states - [to.to_s] }.
|
94
|
+
filter! { |_from_state, to_states| to_states.any? }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
73
98
|
def before_transition(options = {}, &block)
|
74
99
|
add_callback(callback_type: :before, callback_class: Callback,
|
75
100
|
from: options[:from], to: options[:to], &block)
|
@@ -151,6 +176,33 @@ module Statesman
|
|
151
176
|
callback_class.new(from: from, to: to, callback: block)
|
152
177
|
end
|
153
178
|
|
179
|
+
def remove_callbacks(from: nil, to: nil)
|
180
|
+
raise ArgumentError, "Both from and to can't be nil!" if from.nil? && to.nil?
|
181
|
+
return if callbacks.nil?
|
182
|
+
|
183
|
+
@callbacks.transform_values! do |callbacks|
|
184
|
+
filter_callbacks(callbacks, from: from, to: to)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def filter_callbacks(callbacks, from: nil, to: nil)
|
189
|
+
callbacks.filter_map do |callback|
|
190
|
+
next if callback.from == from && to.nil?
|
191
|
+
|
192
|
+
if callback.to.include?(to) && (from.nil? || callback.from == from)
|
193
|
+
next if callback.to == [to]
|
194
|
+
|
195
|
+
callback = Statesman::Callback.new({
|
196
|
+
from: callback.from,
|
197
|
+
to: callback.to - [to],
|
198
|
+
callback: callback.callback,
|
199
|
+
})
|
200
|
+
end
|
201
|
+
|
202
|
+
callback
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
154
206
|
def validate_callback_type_and_class(callback_type, callback_class)
|
155
207
|
raise ArgumentError, "missing keyword: callback_type" if callback_type.nil?
|
156
208
|
raise ArgumentError, "missing keyword: callback_class" if callback_class.nil?
|
@@ -209,6 +261,10 @@ module Statesman
|
|
209
261
|
@storage_adapter.last(force_reload: force_reload)
|
210
262
|
end
|
211
263
|
|
264
|
+
def last_transition_to(state)
|
265
|
+
history.reverse.find { |transition| transition.to_state.to_sym == state.to_sym }
|
266
|
+
end
|
267
|
+
|
212
268
|
def can_transition_to?(new_state, metadata = {})
|
213
269
|
validate_transition(from: current_state,
|
214
270
|
to: new_state,
|
@@ -257,6 +313,10 @@ module Statesman
|
|
257
313
|
false
|
258
314
|
end
|
259
315
|
|
316
|
+
def reset
|
317
|
+
@storage_adapter.reset
|
318
|
+
end
|
319
|
+
|
260
320
|
private
|
261
321
|
|
262
322
|
def adapter_class(transition_class)
|
data/lib/statesman/version.rb
CHANGED
data/lib/statesman.rb
CHANGED
@@ -14,6 +14,8 @@ module Statesman
|
|
14
14
|
"statesman/adapters/active_record_transition"
|
15
15
|
autoload :ActiveRecordQueries,
|
16
16
|
"statesman/adapters/active_record_queries"
|
17
|
+
autoload :TypeSafeActiveRecordQueries,
|
18
|
+
"statesman/adapters/type_safe_active_record_queries"
|
17
19
|
end
|
18
20
|
require "statesman/railtie" if defined?(::Rails::Railtie)
|
19
21
|
|