statesman 0.8.3 → 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|