statesman 0.8.3 → 1.0.0.beta1
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/CHANGELOG.md +14 -0
- data/README.md +111 -3
- data/lib/generators/statesman/templates/create_migration.rb.erb +3 -3
- data/lib/generators/statesman/templates/update_migration.rb.erb +3 -3
- data/lib/statesman/adapters/active_record.rb +5 -1
- data/lib/statesman/adapters/active_record_model.rb +32 -8
- data/lib/statesman/adapters/memory.rb +2 -0
- data/lib/statesman/adapters/mongoid.rb +2 -0
- data/lib/statesman/callback.rb +6 -6
- data/lib/statesman/machine.rb +34 -24
- data/lib/statesman/version.rb +1 -1
- data/spec/generators/statesman/migration_generator_spec.rb +1 -0
- data/spec/statesman/adapters/active_record_model_spec.rb +30 -7
- data/spec/statesman/adapters/active_record_spec.rb +3 -3
- data/spec/statesman/callback_spec.rb +6 -1
- data/spec/statesman/machine_spec.rb +69 -29
- data/spec/support/active_record.rb +16 -0
- data/spec/support/mongoid.rb +2 -0
- data/statesman.gemspec +5 -5
- metadata +15 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 55f5eab4bdf1849056059ca216caa5412679967d
|
4
|
+
data.tar.gz: 56d537766abea0884d94e4a55a9051896bbfc902
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2bead7fc506334da98f4ab620cb6df1fd49012a3a77c3accb8e779e8f09212e3b9926c5ea145ea7d0d3df2f69db13808d05bb65ec55fdfbb39be3ea55d4e915
|
7
|
+
data.tar.gz: e9c8295a8eca68e22debdb594c4802bdd554c18b6a26b4e7d3940fed0673436bf5aa917b4b3a188191b867f6cb4532d08f9383e378d771752ca4b0e91eab1790
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
## v1.0.0.rc1 9 October 2014
|
2
|
+
*Breaking changes*
|
3
|
+
|
4
|
+
- Classes which include `ActiveRecordModel` must define an `initial_state` class method.
|
5
|
+
|
6
|
+
*Fixes*
|
7
|
+
|
8
|
+
- `ActiveRecordModel.in_state` and `ActiveRecordModel.not_in_state` now handle inital states correctly (patch by [@isaacseymour](https://github.com/isaacseymour))
|
9
|
+
|
10
|
+
*Additions*
|
11
|
+
|
12
|
+
- Transition tables created by generated migrations have `NOT NULL` constraints on `to_state`, `sort_key` and foreign key columns (patch by [@greysteil](https://github.com/greysteil))
|
13
|
+
- `before_transition` and `after_transition` allow an array of to states (patch by [@isaacseymour](https://github.com/isaacseymour))
|
14
|
+
|
1
15
|
## v0.8.3 2 September 2014
|
2
16
|
*Fixes*
|
3
17
|
|
data/README.md
CHANGED
@@ -331,7 +331,7 @@ callback code throws an exception, it will not be caught.)
|
|
331
331
|
|
332
332
|
A mixin is provided for the ActiveRecord adapter which adds scopes to easily
|
333
333
|
find all models currently in (or not in) a given state. Include it into your
|
334
|
-
model and define
|
334
|
+
model and define `transition_class` and `initial_state` class methods:
|
335
335
|
|
336
336
|
```ruby
|
337
337
|
class Order < ActiveRecord::Base
|
@@ -342,14 +342,122 @@ class Order < ActiveRecord::Base
|
|
342
342
|
def self.transition_class
|
343
343
|
OrderTransition
|
344
344
|
end
|
345
|
+
|
346
|
+
def self.initial_state
|
347
|
+
OrderStateMachine.initial_state
|
348
|
+
end
|
345
349
|
end
|
346
350
|
```
|
347
351
|
|
348
352
|
#### `Model.in_state(:state_1, :state_2, etc)`
|
349
|
-
Returns all models currently in any of the supplied states.
|
353
|
+
Returns all models currently in any of the supplied states. Prior to 1.0 this ignored all models in the initial state, and the `initial_state` class method was not required.
|
350
354
|
|
351
355
|
#### `Model.not_in_state(:state_1, :state_2, etc)`
|
352
|
-
Returns all models not currently in any of the supplied states.
|
356
|
+
Returns all models not currently in any of the supplied states. Prior to 1.0 this always excluded models in the initial state, and the `initial_state` class method was not required.
|
357
|
+
|
358
|
+
## Frequently Asked Questions
|
359
|
+
|
360
|
+
#### `Model.in_state` does not work before first transition
|
361
|
+
|
362
|
+
This is expected. State is modelled as a series of tranisitions so initially where there has been no transition, there is no explict state. At GoCardless we get around this by transitioning immediately after creation:
|
363
|
+
|
364
|
+
```ruby
|
365
|
+
after_create do
|
366
|
+
state_machine.transition_to! "pending"
|
367
|
+
end
|
368
|
+
```
|
369
|
+
|
370
|
+
#### Storing the state on the model object
|
371
|
+
|
372
|
+
If you wish to store the model state on the model directly, you can keep it up to date using an `after_transition` hook:
|
373
|
+
|
374
|
+
```ruby
|
375
|
+
after_transition do |model, transition|
|
376
|
+
model.state = transition.to_state
|
377
|
+
model.save!
|
378
|
+
end
|
379
|
+
```
|
380
|
+
|
381
|
+
#### Accessing metadata from the last transition
|
382
|
+
|
383
|
+
Given a field `foo` that was stored in the metadata, you can access it like so:
|
384
|
+
|
385
|
+
```ruby
|
386
|
+
model_instance.last_transition.metadata["foo"]
|
387
|
+
```
|
388
|
+
|
389
|
+
## Testing Statesman Implementations
|
390
|
+
|
391
|
+
This answer was abstracted from [this issue](https://github.com/gocardless/statesman/issues/77).
|
392
|
+
|
393
|
+
At GoCardless we focus on testing that:
|
394
|
+
- guards correctly prevent / allow transitions
|
395
|
+
- callbacks execute when expected and perform the expected actions
|
396
|
+
|
397
|
+
#### Testing Guards
|
398
|
+
|
399
|
+
Guards can be tested by asserting that `transition_to!` does or does not raise a `Statesman::GuardFailedError`:
|
400
|
+
|
401
|
+
```ruby
|
402
|
+
describe "guards" do
|
403
|
+
it "cannot transition from state foo to state bar" do
|
404
|
+
expect { some_model.transition_to!(:bar) }.to raise_error(Statesman::GuardFailedError)
|
405
|
+
end
|
406
|
+
|
407
|
+
it "can transition from state foo to state baz" do
|
408
|
+
expect { some_model.transition_to!(:baz).to_not raise_error
|
409
|
+
end
|
410
|
+
end
|
411
|
+
```
|
412
|
+
|
413
|
+
#### Testing Callbacks
|
414
|
+
|
415
|
+
Callbacks are tested by asserting that the action they perform occurs:
|
416
|
+
|
417
|
+
```ruby
|
418
|
+
describe "some callback" do
|
419
|
+
it "adds one to the count property on the model" do
|
420
|
+
expect { some_model.transition_to!(:some_state) }.
|
421
|
+
to change {
|
422
|
+
some_model.reload.count
|
423
|
+
}.by(1)
|
424
|
+
end
|
425
|
+
end
|
426
|
+
```
|
427
|
+
|
428
|
+
#### Creating models in certain states
|
429
|
+
|
430
|
+
Sometimes you'll want to test a guard/transition from one state to another, where the state you want to go from is not the initial state of the model. In this instance you'll need to construct a model instance in the state required. However, if you have strict guards, this can be a pain. One way to get around this in tests is to directly create the transitions in the database, hence avoiding the guards.
|
431
|
+
|
432
|
+
We use [FactoryGirl](https://github.com/thoughtbot/factory_girl) for creating our test objects. Given an `Order` model that is backed by Statesman, we can easily set it up to be in a particular state:
|
433
|
+
|
434
|
+
```ruby
|
435
|
+
factory :order do
|
436
|
+
property "value"
|
437
|
+
...
|
438
|
+
|
439
|
+
trait :shipped do
|
440
|
+
after(:create) do |order|
|
441
|
+
FactoryGirl.create(:order_transition, :shipped, order: order)
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
factory :order_transition do
|
447
|
+
order
|
448
|
+
...
|
449
|
+
|
450
|
+
trait :shipped do
|
451
|
+
to_state "shipped"
|
452
|
+
end
|
453
|
+
end
|
454
|
+
```
|
455
|
+
|
456
|
+
This means you can easily create an `Order` in the `shipped` state:
|
457
|
+
|
458
|
+
```ruby
|
459
|
+
let(:shipped_order) { FactoryGirl.create(:order, :shipped) }
|
460
|
+
```
|
353
461
|
|
354
462
|
---
|
355
463
|
|
@@ -1,10 +1,10 @@
|
|
1
1
|
class Create<%= migration_class_name %> < ActiveRecord::Migration
|
2
2
|
def change
|
3
3
|
create_table :<%= table_name %> do |t|
|
4
|
-
t.string :to_state
|
4
|
+
t.string :to_state, null: false
|
5
5
|
t.text :metadata<%= ", default: \"{}\"" unless mysql? %>
|
6
|
-
t.integer :sort_key
|
7
|
-
t.integer :<%= parent_id
|
6
|
+
t.integer :sort_key, null: false
|
7
|
+
t.integer :<%= parent_id %>, null: false
|
8
8
|
t.timestamps
|
9
9
|
end
|
10
10
|
|
@@ -1,9 +1,9 @@
|
|
1
1
|
class AddStatesmanTo<%= migration_class_name %> < ActiveRecord::Migration
|
2
2
|
def change
|
3
|
-
add_column :<%= table_name %>, :to_state, :string
|
3
|
+
add_column :<%= table_name %>, :to_state, :string, null: false
|
4
4
|
add_column :<%= table_name %>, :metadata, :text<%= ", default: \"{}\"" unless mysql? %>
|
5
|
-
add_column :<%= table_name %>, :sort_key, :integer
|
6
|
-
add_column :<%= table_name %>, :<%= parent_id %>, :integer
|
5
|
+
add_column :<%= table_name %>, :sort_key, :integer, null: false
|
6
|
+
add_column :<%= table_name %>, :<%= parent_id %>, :integer, null: false
|
7
7
|
add_column :<%= table_name %>, :created_at, :datetime
|
8
8
|
add_column :<%= table_name %>, :updated_at, :datetime
|
9
9
|
|
@@ -23,6 +23,8 @@ module Statesman
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def create(from, to, metadata = {})
|
26
|
+
from = from.to_s
|
27
|
+
to = to.to_s
|
26
28
|
create_transition(from, to, metadata)
|
27
29
|
rescue ::ActiveRecord::RecordNotUnique => e
|
28
30
|
if e.message.include?('sort_key') &&
|
@@ -36,7 +38,9 @@ module Statesman
|
|
36
38
|
|
37
39
|
def history
|
38
40
|
if transitions_for_parent.loaded?
|
39
|
-
|
41
|
+
# Workaround for Rails bug which causes infinite loop when sorting
|
42
|
+
# already loaded result set. Introduced in rails/rails@b097ebe
|
43
|
+
transitions_for_parent.to_a.sort_by(&:sort_key)
|
40
44
|
else
|
41
45
|
transitions_for_parent.order(:sort_key)
|
42
46
|
end
|
@@ -7,16 +7,20 @@ module Statesman
|
|
7
7
|
|
8
8
|
module ClassMethods
|
9
9
|
def in_state(*states)
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
states = states.map(&:to_s)
|
11
|
+
|
12
|
+
joins(transition1_join)
|
13
|
+
.joins(transition2_join)
|
14
|
+
.where(state_inclusion_where(states), states)
|
13
15
|
.where("transition2.id" => nil)
|
14
16
|
end
|
15
17
|
|
16
18
|
def not_in_state(*states)
|
17
|
-
|
18
|
-
|
19
|
-
|
19
|
+
states = states.map(&:to_s)
|
20
|
+
|
21
|
+
joins(transition1_join)
|
22
|
+
.joins(transition2_join)
|
23
|
+
.where("NOT (#{state_inclusion_where(states)})", states)
|
20
24
|
.where("transition2.id" => nil)
|
21
25
|
end
|
22
26
|
|
@@ -27,6 +31,11 @@ module Statesman
|
|
27
31
|
"defined on the model"
|
28
32
|
end
|
29
33
|
|
34
|
+
def initial_state
|
35
|
+
raise NotImplementedError, "An initial_state method should be " \
|
36
|
+
"defined on the model"
|
37
|
+
end
|
38
|
+
|
30
39
|
def transition_name
|
31
40
|
transition_class.table_name.to_sym
|
32
41
|
end
|
@@ -35,10 +44,25 @@ module Statesman
|
|
35
44
|
reflections[transition_name].foreign_key
|
36
45
|
end
|
37
46
|
|
38
|
-
def
|
47
|
+
def transition1_join
|
48
|
+
"LEFT OUTER JOIN #{transition_name} transition1
|
49
|
+
ON transition1.#{model_foreign_key} = #{table_name}.id"
|
50
|
+
end
|
51
|
+
|
52
|
+
def transition2_join
|
39
53
|
"LEFT OUTER JOIN #{transition_name} transition2
|
40
54
|
ON transition2.#{model_foreign_key} = #{table_name}.id
|
41
|
-
AND transition2.sort_key >
|
55
|
+
AND transition2.sort_key > transition1.sort_key"
|
56
|
+
end
|
57
|
+
|
58
|
+
def state_inclusion_where(states)
|
59
|
+
if initial_state.in?(states)
|
60
|
+
'transition1.to_state IN (?) OR ' \
|
61
|
+
'transition1.to_state IS NULL'
|
62
|
+
else
|
63
|
+
'transition1.to_state IN (?) AND ' \
|
64
|
+
'transition1.to_state IS NOT NULL'
|
65
|
+
end
|
42
66
|
end
|
43
67
|
end
|
44
68
|
end
|
data/lib/statesman/callback.rb
CHANGED
@@ -11,8 +11,8 @@ module Statesman
|
|
11
11
|
raise InvalidCallbackError, "No callback passed"
|
12
12
|
end
|
13
13
|
|
14
|
-
@from
|
15
|
-
@to
|
14
|
+
@from = options[:from]
|
15
|
+
@to = Array(options[:to])
|
16
16
|
@callback = options[:callback]
|
17
17
|
end
|
18
18
|
|
@@ -34,19 +34,19 @@ module Statesman
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def matches_all_transitions
|
37
|
-
from.nil? && to.
|
37
|
+
from.nil? && to.empty?
|
38
38
|
end
|
39
39
|
|
40
40
|
def matches_from_state(from, to)
|
41
|
-
(from == self.from && (to.nil? || self.to.
|
41
|
+
(from == self.from && (to.nil? || self.to.empty?))
|
42
42
|
end
|
43
43
|
|
44
44
|
def matches_to_state(from, to)
|
45
|
-
((from.nil? || self.from.nil?) &&
|
45
|
+
((from.nil? || self.from.nil?) && self.to.include?(to))
|
46
46
|
end
|
47
47
|
|
48
48
|
def matches_both_states(from, to)
|
49
|
-
from == self.from &&
|
49
|
+
from == self.from && self.to.include?(to)
|
50
50
|
end
|
51
51
|
end
|
52
52
|
end
|
data/lib/statesman/machine.rb
CHANGED
@@ -64,7 +64,7 @@ module Statesman
|
|
64
64
|
|
65
65
|
def transition(options = { from: nil, to: nil }, event = nil)
|
66
66
|
from = to_s_or_nil(options[:from])
|
67
|
-
to =
|
67
|
+
to = array_to_s_or_nil(options[:to])
|
68
68
|
|
69
69
|
raise InvalidStateError, "No to states provided." if to.empty?
|
70
70
|
|
@@ -82,44 +82,39 @@ module Statesman
|
|
82
82
|
end
|
83
83
|
|
84
84
|
def before_transition(options = { from: nil, to: nil }, &block)
|
85
|
-
|
86
|
-
|
85
|
+
add_callback(
|
86
|
+
options.merge(callback_class: Callback, callback_type: :before),
|
87
|
+
&block)
|
88
|
+
end
|
87
89
|
|
88
|
-
|
89
|
-
|
90
|
+
def guard_transition(options = { from: nil, to: nil }, &block)
|
91
|
+
add_callback(
|
92
|
+
options.merge(callback_class: Guard, callback_type: :guards),
|
93
|
+
&block)
|
90
94
|
end
|
91
95
|
|
92
96
|
def after_transition(options = { from: nil, to: nil,
|
93
97
|
after_commit: false }, &block)
|
94
|
-
|
95
|
-
to = to_s_or_nil(options[:to])
|
98
|
+
callback_type = options[:after_commit] ? :after_commit : :after
|
96
99
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
end
|
101
|
-
|
102
|
-
def guard_transition(options = { from: nil, to: nil }, &block)
|
103
|
-
from = to_s_or_nil(options[:from])
|
104
|
-
to = to_s_or_nil(options[:to])
|
105
|
-
|
106
|
-
validate_callback_condition(from: from, to: to)
|
107
|
-
callbacks[:guards] << Guard.new(from: from, to: to, callback: block)
|
100
|
+
add_callback(
|
101
|
+
options.merge(callback_class: Callback, callback_type: callback_type),
|
102
|
+
&block)
|
108
103
|
end
|
109
104
|
|
110
105
|
def validate_callback_condition(options = { from: nil, to: nil })
|
111
106
|
from = to_s_or_nil(options[:from])
|
112
|
-
to =
|
107
|
+
to = array_to_s_or_nil(options[:to])
|
113
108
|
|
114
|
-
[from
|
115
|
-
return if from.nil? && to.
|
109
|
+
([from] + to).compact.each { |state| validate_state(state) }
|
110
|
+
return if from.nil? && to.empty?
|
116
111
|
|
117
112
|
validate_not_from_terminal_state(from)
|
118
|
-
validate_not_to_initial_state(
|
113
|
+
to.each { |state| validate_not_to_initial_state(state) }
|
119
114
|
|
120
|
-
return if from.nil? || to.
|
115
|
+
return if from.nil? || to.empty?
|
121
116
|
|
122
|
-
validate_from_and_to_state(from,
|
117
|
+
to.each { |state| validate_from_and_to_state(from, state) }
|
123
118
|
end
|
124
119
|
|
125
120
|
# Check that the 'from' state is not terminal
|
@@ -148,6 +143,17 @@ module Statesman
|
|
148
143
|
|
149
144
|
private
|
150
145
|
|
146
|
+
def add_callback(options, &block)
|
147
|
+
from = to_s_or_nil(options[:from])
|
148
|
+
to = array_to_s_or_nil(options[:to])
|
149
|
+
callback_klass = options.fetch(:callback_class)
|
150
|
+
callback_type = options.fetch(:callback_type)
|
151
|
+
|
152
|
+
validate_callback_condition(from: from, to: to)
|
153
|
+
callbacks[callback_type] <<
|
154
|
+
callback_klass.new(from: from, to: to, callback: block)
|
155
|
+
end
|
156
|
+
|
151
157
|
def validate_state(state)
|
152
158
|
unless states.include?(state.to_s)
|
153
159
|
raise InvalidStateError, "Invalid state '#{state}'"
|
@@ -164,6 +170,10 @@ module Statesman
|
|
164
170
|
def to_s_or_nil(input)
|
165
171
|
input.nil? ? input : input.to_s
|
166
172
|
end
|
173
|
+
|
174
|
+
def array_to_s_or_nil(input)
|
175
|
+
Array(input).map { |item| to_s_or_nil(item) }
|
176
|
+
end
|
167
177
|
end
|
168
178
|
|
169
179
|
def initialize(object,
|
data/lib/statesman/version.rb
CHANGED
@@ -12,46 +12,69 @@ describe Statesman::Adapters::ActiveRecordModel do
|
|
12
12
|
def self.transition_class
|
13
13
|
MyActiveRecordModelTransition
|
14
14
|
end
|
15
|
+
|
16
|
+
def self.initial_state
|
17
|
+
MyStateMachine.initial_state
|
18
|
+
end
|
15
19
|
end
|
16
20
|
end
|
17
21
|
|
18
22
|
let!(:model) do
|
19
23
|
model = MyActiveRecordModel.create
|
20
|
-
model.my_active_record_model_transitions.create(to_state: :
|
24
|
+
model.my_active_record_model_transitions.create(to_state: :succeeded)
|
21
25
|
model
|
22
26
|
end
|
23
27
|
|
24
28
|
let!(:other_model) do
|
25
29
|
model = MyActiveRecordModel.create
|
26
|
-
model.my_active_record_model_transitions.create(to_state: :
|
30
|
+
model.my_active_record_model_transitions.create(to_state: :failed)
|
31
|
+
model
|
32
|
+
end
|
33
|
+
|
34
|
+
let!(:initial_state_model) { MyActiveRecordModel.create }
|
35
|
+
|
36
|
+
let!(:returned_to_initial_model) do
|
37
|
+
model = MyActiveRecordModel.create
|
38
|
+
model.my_active_record_model_transitions.create(to_state: :failed)
|
39
|
+
model.my_active_record_model_transitions.create(to_state: :initial)
|
27
40
|
model
|
28
41
|
end
|
29
42
|
|
30
43
|
describe ".in_state" do
|
31
44
|
context "given a single state" do
|
32
|
-
subject { MyActiveRecordModel.in_state(:
|
45
|
+
subject { MyActiveRecordModel.in_state(:succeeded) }
|
33
46
|
|
34
47
|
it { is_expected.to include model }
|
35
48
|
end
|
36
49
|
|
37
50
|
context "given multiple states" do
|
38
|
-
subject { MyActiveRecordModel.in_state(:
|
51
|
+
subject { MyActiveRecordModel.in_state(:succeeded, :failed) }
|
39
52
|
|
40
53
|
it { is_expected.to include model }
|
41
54
|
it { is_expected.to include other_model }
|
42
55
|
end
|
56
|
+
|
57
|
+
context "given the initial state" do
|
58
|
+
subject { MyActiveRecordModel.in_state(:initial) }
|
59
|
+
|
60
|
+
it { is_expected.to include initial_state_model }
|
61
|
+
it { is_expected.to include returned_to_initial_model }
|
62
|
+
end
|
43
63
|
end
|
44
64
|
|
45
65
|
describe ".not_in_state" do
|
46
66
|
context "given a single state" do
|
47
|
-
subject { MyActiveRecordModel.not_in_state(:
|
67
|
+
subject { MyActiveRecordModel.not_in_state(:failed) }
|
48
68
|
it { is_expected.to include model }
|
49
69
|
it { is_expected.not_to include other_model }
|
50
70
|
end
|
51
71
|
|
52
72
|
context "given multiple states" do
|
53
|
-
subject { MyActiveRecordModel.not_in_state(:
|
54
|
-
it
|
73
|
+
subject { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
|
74
|
+
it do
|
75
|
+
is_expected.to match_array([initial_state_model,
|
76
|
+
returned_to_initial_model])
|
77
|
+
end
|
55
78
|
end
|
56
79
|
end
|
57
80
|
end
|
@@ -56,7 +56,7 @@ describe Statesman::Adapters::ActiveRecord do
|
|
56
56
|
end
|
57
57
|
|
58
58
|
describe "#create" do
|
59
|
-
let(:adapter) do
|
59
|
+
let!(:adapter) do
|
60
60
|
described_class.new(MyActiveRecordModelTransition, model, observer)
|
61
61
|
end
|
62
62
|
let(:from) { :x }
|
@@ -123,8 +123,8 @@ describe Statesman::Adapters::ActiveRecord do
|
|
123
123
|
|
124
124
|
context "with a pre-fetched transition history" do
|
125
125
|
before do
|
126
|
-
|
127
|
-
model.my_active_record_model_transitions.
|
126
|
+
adapter.create(:x, :y)
|
127
|
+
model.my_active_record_model_transitions.load_target
|
128
128
|
end
|
129
129
|
|
130
130
|
it "doesn't query the database" do
|
@@ -72,7 +72,7 @@ describe Statesman::Callback do
|
|
72
72
|
|
73
73
|
context "with any from value on the callback" do
|
74
74
|
let(:callback) do
|
75
|
-
Statesman::Callback.new(to: :y, callback: cb_lambda)
|
75
|
+
Statesman::Callback.new(to: [:y, :z], callback: cb_lambda)
|
76
76
|
end
|
77
77
|
let(:from) { :x }
|
78
78
|
|
@@ -81,6 +81,11 @@ describe Statesman::Callback do
|
|
81
81
|
it { is_expected.to be_truthy }
|
82
82
|
end
|
83
83
|
|
84
|
+
context "and another allowed to value" do
|
85
|
+
let(:to) { :z }
|
86
|
+
it { is_expected.to be_truthy }
|
87
|
+
end
|
88
|
+
|
84
89
|
context "and a disallowed to value" do
|
85
90
|
let(:to) { :a }
|
86
91
|
it { is_expected.to be_falsey }
|
@@ -218,54 +218,94 @@ describe Statesman::Machine do
|
|
218
218
|
machine.class_eval do
|
219
219
|
state :x, initial: true
|
220
220
|
state :y
|
221
|
-
|
221
|
+
state :z
|
222
|
+
transition from: :x, to: [:y, :z]
|
222
223
|
end
|
223
224
|
end
|
224
225
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
226
|
+
let(:options) { { from: nil, to: [] } }
|
227
|
+
let(:set_callback) { machine.send(assignment_method, options) {} }
|
228
|
+
|
229
|
+
shared_examples "fails" do |error_type|
|
230
|
+
it "raises an exception" do
|
231
|
+
expect { set_callback }.to raise_error(error_type)
|
232
|
+
end
|
233
|
+
|
234
|
+
it "does not add a callback" do
|
235
|
+
expect do
|
236
|
+
begin
|
237
|
+
set_callback
|
238
|
+
rescue error_type
|
239
|
+
nil
|
240
|
+
end
|
241
|
+
end.to_not change(machine.callbacks[callback_store], :count)
|
242
|
+
end
|
229
243
|
end
|
230
244
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
245
|
+
shared_examples "adds callback" do
|
246
|
+
it "does not raise" do
|
247
|
+
expect { set_callback }.to_not raise_error
|
248
|
+
end
|
249
|
+
|
250
|
+
it "stores callbacks" do
|
251
|
+
expect { set_callback }.to change(
|
252
|
+
machine.callbacks[callback_store], :count).by(1)
|
253
|
+
end
|
254
|
+
|
255
|
+
it "stores callback instances" do
|
256
|
+
set_callback
|
257
|
+
machine.callbacks[callback_store].each do |callback|
|
258
|
+
expect(callback).to be_a(Statesman::Callback)
|
259
|
+
end
|
235
260
|
end
|
236
261
|
end
|
237
262
|
|
238
263
|
context "with invalid states" do
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
end.to raise_error(Statesman::InvalidStateError)
|
264
|
+
context "when both are invalid" do
|
265
|
+
let(:options) { { from: :foo, to: :bar } }
|
266
|
+
it_behaves_like "fails", Statesman::InvalidStateError
|
243
267
|
end
|
244
268
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
end.to raise_error(Statesman::InvalidTransitionError)
|
269
|
+
context "from a terminal state to anything" do
|
270
|
+
let(:options) { { from: :y, to: [] } }
|
271
|
+
it_behaves_like "fails", Statesman::InvalidTransitionError
|
249
272
|
end
|
250
273
|
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
274
|
+
context "to an initial state and from anything" do
|
275
|
+
let(:options) { { from: nil, to: :x } }
|
276
|
+
it_behaves_like "fails", Statesman::InvalidTransitionError
|
277
|
+
end
|
278
|
+
|
279
|
+
context "from a terminal state and to multiple states" do
|
280
|
+
let(:options) { { from: :y, to: [:x, :z] } }
|
281
|
+
it_behaves_like "fails", Statesman::InvalidTransitionError
|
282
|
+
end
|
283
|
+
|
284
|
+
context "to an initial state and other states" do
|
285
|
+
let(:options) { { from: nil, to: [:y, :x, :z] } }
|
286
|
+
it_behaves_like "fails", Statesman::InvalidTransitionError
|
255
287
|
end
|
256
288
|
end
|
257
289
|
|
258
290
|
context "with validate_states" do
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
end.to_not raise_error
|
291
|
+
context "from anything" do
|
292
|
+
let(:options) { { from: nil, to: :y } }
|
293
|
+
it_behaves_like "adds callback"
|
263
294
|
end
|
264
295
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
296
|
+
context "to anything" do
|
297
|
+
let(:options) { { from: :x, to: [] } }
|
298
|
+
it_behaves_like "adds callback"
|
299
|
+
end
|
300
|
+
|
301
|
+
context "to several" do
|
302
|
+
let(:options) { { from: :x, to: [:y, :z] } }
|
303
|
+
it_behaves_like "adds callback"
|
304
|
+
end
|
305
|
+
|
306
|
+
context "from any to several" do
|
307
|
+
let(:options) { { from: nil, to: [:y, :z] } }
|
308
|
+
it_behaves_like "adds callback"
|
269
309
|
end
|
270
310
|
end
|
271
311
|
end
|
@@ -3,8 +3,24 @@ require "json"
|
|
3
3
|
|
4
4
|
DB = Pathname.new("test.sqlite3")
|
5
5
|
|
6
|
+
class MyStateMachine
|
7
|
+
include Statesman::Machine
|
8
|
+
|
9
|
+
state :initial, initial: true
|
10
|
+
state :succeeded
|
11
|
+
state :failed
|
12
|
+
|
13
|
+
transition from: :initial, to: [:succeeded, :failed]
|
14
|
+
transition from: :failed, to: :initial
|
15
|
+
end
|
16
|
+
|
6
17
|
class MyActiveRecordModel < ActiveRecord::Base
|
7
18
|
has_many :my_active_record_model_transitions
|
19
|
+
|
20
|
+
def state_machine
|
21
|
+
@state_machine ||= MyStateMachine
|
22
|
+
.new(self, transition_class: MyActiveRecordModelTransition)
|
23
|
+
end
|
8
24
|
end
|
9
25
|
|
10
26
|
class MyActiveRecordModelTransition < ActiveRecord::Base
|
data/spec/support/mongoid.rb
CHANGED
data/statesman.gemspec
CHANGED
@@ -20,13 +20,13 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_development_dependency "bundler", "~> 1.3"
|
22
22
|
spec.add_development_dependency "rake"
|
23
|
-
spec.add_development_dependency "rspec", "~> 3.
|
23
|
+
spec.add_development_dependency "rspec", "~> 3.1"
|
24
24
|
spec.add_development_dependency "rspec-its", "~> 1.0"
|
25
25
|
spec.add_development_dependency "guard-rspec", "~> 4.3"
|
26
|
-
spec.add_development_dependency "rubocop", "~> 0.
|
26
|
+
spec.add_development_dependency "rubocop", "~> 0.26"
|
27
27
|
spec.add_development_dependency "guard-rubocop", "~> 1.1"
|
28
|
-
spec.add_development_dependency "sqlite3", "~> 1.3
|
29
|
-
spec.add_development_dependency "mongoid", "~>
|
30
|
-
spec.add_development_dependency "
|
28
|
+
spec.add_development_dependency "sqlite3", "~> 1.3"
|
29
|
+
spec.add_development_dependency "mongoid", "~> 4.0"
|
30
|
+
spec.add_development_dependency "activerecord", "~> 4.1"
|
31
31
|
spec.add_development_dependency "ammeter", "~> 1.1"
|
32
32
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: statesman
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0.beta1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Harry Marr
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-09
|
12
|
+
date: 2014-10-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -45,14 +45,14 @@ dependencies:
|
|
45
45
|
requirements:
|
46
46
|
- - ~>
|
47
47
|
- !ruby/object:Gem::Version
|
48
|
-
version: '3.
|
48
|
+
version: '3.1'
|
49
49
|
type: :development
|
50
50
|
prerelease: false
|
51
51
|
version_requirements: !ruby/object:Gem::Requirement
|
52
52
|
requirements:
|
53
53
|
- - ~>
|
54
54
|
- !ruby/object:Gem::Version
|
55
|
-
version: '3.
|
55
|
+
version: '3.1'
|
56
56
|
- !ruby/object:Gem::Dependency
|
57
57
|
name: rspec-its
|
58
58
|
requirement: !ruby/object:Gem::Requirement
|
@@ -87,14 +87,14 @@ dependencies:
|
|
87
87
|
requirements:
|
88
88
|
- - ~>
|
89
89
|
- !ruby/object:Gem::Version
|
90
|
-
version: 0.
|
90
|
+
version: '0.26'
|
91
91
|
type: :development
|
92
92
|
prerelease: false
|
93
93
|
version_requirements: !ruby/object:Gem::Requirement
|
94
94
|
requirements:
|
95
95
|
- - ~>
|
96
96
|
- !ruby/object:Gem::Version
|
97
|
-
version: 0.
|
97
|
+
version: '0.26'
|
98
98
|
- !ruby/object:Gem::Dependency
|
99
99
|
name: guard-rubocop
|
100
100
|
requirement: !ruby/object:Gem::Requirement
|
@@ -115,42 +115,42 @@ dependencies:
|
|
115
115
|
requirements:
|
116
116
|
- - ~>
|
117
117
|
- !ruby/object:Gem::Version
|
118
|
-
version: 1.3
|
118
|
+
version: '1.3'
|
119
119
|
type: :development
|
120
120
|
prerelease: false
|
121
121
|
version_requirements: !ruby/object:Gem::Requirement
|
122
122
|
requirements:
|
123
123
|
- - ~>
|
124
124
|
- !ruby/object:Gem::Version
|
125
|
-
version: 1.3
|
125
|
+
version: '1.3'
|
126
126
|
- !ruby/object:Gem::Dependency
|
127
127
|
name: mongoid
|
128
128
|
requirement: !ruby/object:Gem::Requirement
|
129
129
|
requirements:
|
130
130
|
- - ~>
|
131
131
|
- !ruby/object:Gem::Version
|
132
|
-
version:
|
132
|
+
version: '4.0'
|
133
133
|
type: :development
|
134
134
|
prerelease: false
|
135
135
|
version_requirements: !ruby/object:Gem::Requirement
|
136
136
|
requirements:
|
137
137
|
- - ~>
|
138
138
|
- !ruby/object:Gem::Version
|
139
|
-
version:
|
139
|
+
version: '4.0'
|
140
140
|
- !ruby/object:Gem::Dependency
|
141
|
-
name:
|
141
|
+
name: activerecord
|
142
142
|
requirement: !ruby/object:Gem::Requirement
|
143
143
|
requirements:
|
144
144
|
- - ~>
|
145
145
|
- !ruby/object:Gem::Version
|
146
|
-
version: '
|
146
|
+
version: '4.1'
|
147
147
|
type: :development
|
148
148
|
prerelease: false
|
149
149
|
version_requirements: !ruby/object:Gem::Requirement
|
150
150
|
requirements:
|
151
151
|
- - ~>
|
152
152
|
- !ruby/object:Gem::Version
|
153
|
-
version: '
|
153
|
+
version: '4.1'
|
154
154
|
- !ruby/object:Gem::Dependency
|
155
155
|
name: ammeter
|
156
156
|
requirement: !ruby/object:Gem::Requirement
|
@@ -240,9 +240,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
240
240
|
version: '0'
|
241
241
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
242
242
|
requirements:
|
243
|
-
- - '
|
243
|
+
- - '>'
|
244
244
|
- !ruby/object:Gem::Version
|
245
|
-
version:
|
245
|
+
version: 1.3.1
|
246
246
|
requirements: []
|
247
247
|
rubyforge_project:
|
248
248
|
rubygems_version: 2.4.1
|