flow_machine 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +177 -0
- data/Rakefile +38 -0
- data/lib/flow_machine.rb +12 -0
- data/lib/flow_machine/callback.rb +43 -0
- data/lib/flow_machine/change_callback.rb +11 -0
- data/lib/flow_machine/factory.rb +27 -0
- data/lib/flow_machine/state_callback.rb +5 -0
- data/lib/flow_machine/version.rb +3 -0
- data/lib/flow_machine/workflow.rb +206 -0
- data/lib/flow_machine/workflow_state.rb +162 -0
- data/lib/tasks/workflow_tasks.rake +4 -0
- data/spec/flow_machine/factory_spec.rb +48 -0
- data/spec/flow_machine/multiple_workflow_spec.rb +37 -0
- data/spec/flow_machine/workflow_callback_spec.rb +131 -0
- data/spec/flow_machine/workflow_change_callback_spec.rb +26 -0
- data/spec/flow_machine/workflow_spec.rb +202 -0
- data/spec/flow_machine/workflow_state_spec.rb +253 -0
- data/spec/spec_helper.rb +94 -0
- metadata +112 -0
@@ -0,0 +1,162 @@
|
|
1
|
+
require "active_support/core_ext/module/delegation"
|
2
|
+
require "active_support/inflector"
|
3
|
+
|
4
|
+
class FlowMachine::WorkflowState
|
5
|
+
attr_reader :workflow
|
6
|
+
attr_accessor :guard_errors
|
7
|
+
|
8
|
+
delegate :object, :options, to: :workflow
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
attr_accessor :state_callbacks
|
12
|
+
attr_accessor :expose_to_workflow_methods
|
13
|
+
|
14
|
+
def state_name
|
15
|
+
name.demodulize.sub(/State\z/, '').underscore.to_sym
|
16
|
+
end
|
17
|
+
|
18
|
+
# Maintains a list of methods that should be exposed to the workflow
|
19
|
+
# the workflow is responsible for reading this list
|
20
|
+
def expose_to_workflow(name)
|
21
|
+
self.expose_to_workflow_methods ||= []
|
22
|
+
self.expose_to_workflow_methods << name
|
23
|
+
end
|
24
|
+
|
25
|
+
def event(name, options = {}, &block)
|
26
|
+
define_may_event(name, options)
|
27
|
+
define_event(name, options, &block)
|
28
|
+
define_event_bang(name)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def define_event(name, options, &block)
|
34
|
+
define_method name do |*args|
|
35
|
+
return false unless self.send("may_#{name}?")
|
36
|
+
instance_exec *args, &block
|
37
|
+
end
|
38
|
+
expose_to_workflow name
|
39
|
+
end
|
40
|
+
|
41
|
+
def define_may_event(name, options)
|
42
|
+
define_method "may_#{name}?" do
|
43
|
+
run_guard_methods([*options[:guard]])
|
44
|
+
end
|
45
|
+
expose_to_workflow "may_#{name}?"
|
46
|
+
end
|
47
|
+
|
48
|
+
def define_event_bang(name)
|
49
|
+
define_method "#{name}!" do |*args|
|
50
|
+
workflow.persist if self.send(name, *args)
|
51
|
+
end
|
52
|
+
expose_to_workflow "#{name}!"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
extend ClassMethods
|
56
|
+
|
57
|
+
# Callbacks may be a symbol method name on the state, workflow, or underlying object,
|
58
|
+
# and will look for that method on those objects in that order. You may also
|
59
|
+
# use a block.
|
60
|
+
# Callbacks will accept :if and :unless options, which also may be method name
|
61
|
+
# symbols or blocks. The option accepts an array meaning all methods must return
|
62
|
+
# true (for if) and false (for unless)
|
63
|
+
#
|
64
|
+
# class ExampleState < Workflow::State
|
65
|
+
# on_enter :some_method, if: :allowed?
|
66
|
+
# after_enter :after_enter_method, if: [:this_is_true?, :and_this_is_true?]
|
67
|
+
# before_change(:field_name) { do_something }
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
module CallbackDsl
|
71
|
+
# Called when the workflow `transition`s to the state
|
72
|
+
def on_enter(*args, &block)
|
73
|
+
add_callback(:on_enter, FlowMachine::StateCallback.new(*args, &block))
|
74
|
+
end
|
75
|
+
|
76
|
+
# Called after `persist` when the workflow transitioned into this state
|
77
|
+
def after_enter(*args, &block)
|
78
|
+
add_callback(:after_enter, FlowMachine::StateCallback.new(*args, &block))
|
79
|
+
end
|
80
|
+
|
81
|
+
# Happens before persistence if the field on the object has changed
|
82
|
+
def before_change(field, *args, &block)
|
83
|
+
add_callback(:before_change, FlowMachine::ChangeCallback.new(field, *args, &block))
|
84
|
+
end
|
85
|
+
|
86
|
+
# Happens after persistence if the field on the object has changed
|
87
|
+
def after_change(field, *args, &block)
|
88
|
+
add_callback(:after_change, FlowMachine::ChangeCallback.new(field, *args, &block))
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def add_callback(hook, callback)
|
94
|
+
self.state_callbacks ||= {}
|
95
|
+
state_callbacks[hook] ||= []
|
96
|
+
state_callbacks[hook] << callback
|
97
|
+
end
|
98
|
+
end
|
99
|
+
extend CallbackDsl
|
100
|
+
|
101
|
+
def initialize(workflow)
|
102
|
+
@workflow = workflow
|
103
|
+
@guard_errors = []
|
104
|
+
end
|
105
|
+
|
106
|
+
def fire_callback_list(callbacks, changes = {})
|
107
|
+
callbacks.each do |callback|
|
108
|
+
callback.call(self, changes)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def fire_callbacks(event, changes = {})
|
113
|
+
return unless self.class.state_callbacks.try(:[], event)
|
114
|
+
fire_callback_list self.class.state_callbacks[event], changes
|
115
|
+
end
|
116
|
+
|
117
|
+
# Allows method calls to fallback up the object chain so
|
118
|
+
# guards and other methods can be defined on the object or workflow
|
119
|
+
# as well as the state
|
120
|
+
def run_workflow_method(method_name, *args, &block)
|
121
|
+
if target = object_chain(method_name)
|
122
|
+
target.send(method_name, *args, &block)
|
123
|
+
else
|
124
|
+
raise NoMethodError.new("undefined method #{method_name}", method_name)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def transition(options = {})
|
129
|
+
workflow.transition(options).tap do |new_state|
|
130
|
+
new_state.fire_callbacks(:on_enter) if new_state
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def name
|
135
|
+
self.class.state_name
|
136
|
+
end
|
137
|
+
|
138
|
+
def ==(other)
|
139
|
+
self.class == other.class
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def run_guard_methods(guard_methods)
|
145
|
+
self.guard_errors = []
|
146
|
+
# Use inject to ensure that all guard methods are run.
|
147
|
+
# all? short circuits on first false value
|
148
|
+
guard_methods.inject(true) do |valid, guard_method|
|
149
|
+
if self.run_workflow_method(guard_method)
|
150
|
+
valid
|
151
|
+
else
|
152
|
+
self.guard_errors << guard_method
|
153
|
+
false
|
154
|
+
end
|
155
|
+
end
|
156
|
+
#
|
157
|
+
end
|
158
|
+
|
159
|
+
def object_chain(method_name)
|
160
|
+
[self, workflow, object].find { |o| o.respond_to?(method_name, true) }
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
RSpec.describe FlowMachine::Factory do
|
2
|
+
class TestClass; end
|
3
|
+
|
4
|
+
class TestClassWorkflow
|
5
|
+
include FlowMachine::Workflow
|
6
|
+
end
|
7
|
+
|
8
|
+
describe '.workflow_class_for' do
|
9
|
+
subject(:workflow_class) { described_class.workflow_class_for(target) }
|
10
|
+
|
11
|
+
describe 'with a class' do
|
12
|
+
let(:target) { TestClass }
|
13
|
+
it { should eq(TestClassWorkflow) }
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'with an object' do
|
17
|
+
let(:target) { TestClass.new }
|
18
|
+
it { should eq(TestClassWorkflow) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '.workflow_for' do
|
23
|
+
subject(:workflow) { described_class.workflow_for(target) }
|
24
|
+
class SomeNewClass; end
|
25
|
+
|
26
|
+
describe 'not found' do
|
27
|
+
let(:target) { SomeNewClass.new }
|
28
|
+
it { should be_nil }
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'with an object' do
|
32
|
+
let(:target) { TestClass.new }
|
33
|
+
it { should be_an_instance_of(TestClassWorkflow) }
|
34
|
+
its(:object) { should eq(target) }
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'with an array of objects' do
|
38
|
+
let(:target) { [TestClass.new, TestClass.new] }
|
39
|
+
it { should match [an_instance_of(TestClassWorkflow), an_instance_of(TestClassWorkflow)] }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '.workflow_for_collection' do
|
44
|
+
subject(:result) { described_class.workflow_for_collection(target) }
|
45
|
+
let(:target) { [TestClass.new, TestClass.new] }
|
46
|
+
it { should match [an_instance_of(TestClassWorkflow), an_instance_of(TestClassWorkflow)] }
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
RSpec.describe FlowMachine::Workflow do
|
2
|
+
let(:state_class1) do
|
3
|
+
Class.new(FlowMachine::WorkflowState) do
|
4
|
+
def self.state_name; :state1; end
|
5
|
+
event :event1
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:state_class2) do
|
10
|
+
Class.new(FlowMachine::WorkflowState) do
|
11
|
+
def self.state_name; :state2; end
|
12
|
+
event :event2
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:workflow_class1) { Class.new }
|
17
|
+
let(:workflow_class2) { Class.new }
|
18
|
+
|
19
|
+
before do
|
20
|
+
workflow_class1.include described_class
|
21
|
+
workflow_class2.include described_class
|
22
|
+
workflow_class1.state state_class1
|
23
|
+
workflow_class2.state state_class2
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'class1' do
|
27
|
+
subject { workflow_class1.new(double) }
|
28
|
+
it { should respond_to 'event1' }
|
29
|
+
it { should_not respond_to 'event2' }
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'class2' do
|
33
|
+
subject { workflow_class2.new(double) }
|
34
|
+
it { should_not respond_to 'event1' }
|
35
|
+
it { should respond_to 'event2' }
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
RSpec.describe FlowMachine::Callback do
|
2
|
+
let(:object) { double }
|
3
|
+
|
4
|
+
describe 'callback with method' do
|
5
|
+
let(:callback) { described_class.new(:some_method) }
|
6
|
+
|
7
|
+
it 'calls the method' do
|
8
|
+
expect(object).to receive(:some_method)
|
9
|
+
callback.call(object)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe 'callback with block' do
|
14
|
+
let(:callback) do
|
15
|
+
described_class.new { some_method }
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'calls the method' do
|
19
|
+
expect(object).to receive(:some_method)
|
20
|
+
callback.call(object)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'callback with if' do
|
26
|
+
context 'a single if' do
|
27
|
+
let(:callback) { described_class.new(:some_method, if: :if_method?) }
|
28
|
+
|
29
|
+
context 'if method returns true' do
|
30
|
+
before { allow(object).to receive(:if_method?).and_return true }
|
31
|
+
it 'calls the method' do
|
32
|
+
expect(object).to receive(:some_method)
|
33
|
+
callback.call(object)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'if method returns false' do
|
38
|
+
before { allow(object).to receive(:if_method?).and_return false }
|
39
|
+
it 'does not call the method' do
|
40
|
+
expect(object).not_to receive(:some_method)
|
41
|
+
callback.call(object)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'a lambda for if' do
|
47
|
+
let(:callback) do
|
48
|
+
described_class.new(:some_method, if: -> { if_method? })
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'calls the method when true' do
|
52
|
+
allow(object).to receive(:if_method?).and_return true
|
53
|
+
expect(object).to receive(:some_method)
|
54
|
+
callback.call(object)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'does not call the method when false' do
|
58
|
+
allow(object).to receive(:if_method?).and_return false
|
59
|
+
expect(object).not_to receive(:some_method)
|
60
|
+
callback.call(object)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'an array of ifs' do
|
65
|
+
let(:callback) { described_class.new(:some_method, if: [:if_method?, :if2?]) }
|
66
|
+
context 'both return true' do
|
67
|
+
before :each do
|
68
|
+
allow(object).to receive(:if_method?).and_return true
|
69
|
+
allow(object).to receive(:if2?).and_return true
|
70
|
+
end
|
71
|
+
it 'calls the method' do
|
72
|
+
expect(object).to receive(:some_method)
|
73
|
+
callback.call(object)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'one returns false' do
|
78
|
+
before :each do
|
79
|
+
allow(object).to receive(:if_method?).and_return true
|
80
|
+
allow(object).to receive(:if2?).and_return false
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'does not call the method' do
|
84
|
+
expect(object).to receive(:some_method).never
|
85
|
+
callback.call(object)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe 'callback with unless' do
|
92
|
+
context 'a single if' do
|
93
|
+
let(:callback) { described_class.new(:some_method, unless: :unless_method?) }
|
94
|
+
|
95
|
+
context 'unless method returns false' do
|
96
|
+
before { allow(object).to receive(:unless_method?).and_return false }
|
97
|
+
|
98
|
+
it 'calls the method' do
|
99
|
+
expect(object).to receive(:some_method)
|
100
|
+
callback.call(object)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context 'unless method returns true' do
|
105
|
+
before { allow(object).to receive(:unless_method?).and_return true }
|
106
|
+
it 'does not call the method' do
|
107
|
+
expect(object).to receive(:some_method).never
|
108
|
+
callback.call(object)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'a lambda for unless' do
|
114
|
+
let(:callback) do
|
115
|
+
described_class.new(:some_method, unless: -> { unless_method? })
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'calls the method when false' do
|
119
|
+
allow(object).to receive(:unless_method?).and_return false
|
120
|
+
expect(object).to receive(:some_method)
|
121
|
+
callback.call(object)
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'does not call the method when true' do
|
125
|
+
allow(object).to receive(:unless_method?).and_return true
|
126
|
+
expect(object).not_to receive(:some_method)
|
127
|
+
callback.call(object)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
RSpec.describe FlowMachine::ChangeCallback do
|
2
|
+
subject(:callback) { described_class.new(:field, :method, if: :condition?) }
|
3
|
+
|
4
|
+
specify { expect(callback.field).to eq(:field) }
|
5
|
+
specify { expect(callback.method).to eq(:method) }
|
6
|
+
specify { expect(callback.options).to eq({ if: :condition? }) }
|
7
|
+
|
8
|
+
let(:object) { double(condition?: true) }
|
9
|
+
before { allow(object).to receive(:run_workflow_method) { |m| object.send(m) } }
|
10
|
+
|
11
|
+
context 'the field changes' do
|
12
|
+
let(:changes) { { 'field' => [:old, :new] } }
|
13
|
+
it 'calls the method' do
|
14
|
+
expect(object).to receive(:method)
|
15
|
+
callback.call(object, changes)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'the field does not change' do
|
20
|
+
let(:changes) { { 'other_field' => [:old, :new] } }
|
21
|
+
it 'does not call the method' do
|
22
|
+
expect(object).to receive(:method).never
|
23
|
+
callback.call(object, changes)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
RSpec.describe FlowMachine::Workflow do
|
4
|
+
class Test1State < FlowMachine::WorkflowState
|
5
|
+
def state_method; end
|
6
|
+
end
|
7
|
+
|
8
|
+
class Test2State < FlowMachine::WorkflowState
|
9
|
+
def state_method; end
|
10
|
+
end
|
11
|
+
|
12
|
+
class TestWorkflow
|
13
|
+
include FlowMachine::Workflow
|
14
|
+
state Test1State
|
15
|
+
state Test2State
|
16
|
+
before_save :before_save_callback
|
17
|
+
after_save :after_save_callback
|
18
|
+
after_transition :after_transition_callback
|
19
|
+
def workflow_method; end
|
20
|
+
def before_save_callback; end
|
21
|
+
def after_save_callback; end
|
22
|
+
def after_transition_callback; end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'state_names' do
|
26
|
+
it 'has the state names' do
|
27
|
+
expect(TestWorkflow.state_names).to eq(['test1', 'test2'])
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'has the states in a hash keyed by names' do
|
31
|
+
expect(TestWorkflow.states).to eq({ test1: Test1State, test2: Test2State })
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'reading initial state' do
|
36
|
+
subject(:workflow) { TestWorkflow.new(object) }
|
37
|
+
context 'default state field' do
|
38
|
+
let(:object) { double(state: :test1) }
|
39
|
+
it 'has the state' do
|
40
|
+
expect(workflow.current_state_name).to eq(:test1)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'with a different state field on the object' do
|
45
|
+
before { TestWorkflow.send(:state_attribute, :status) }
|
46
|
+
# return to default
|
47
|
+
after { TestWorkflow.send(:state_attribute, :state) }
|
48
|
+
|
49
|
+
let(:object) { double(status: :test2) }
|
50
|
+
it 'has the state' do
|
51
|
+
expect(workflow.current_state_name).to eq(:test2)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#transition' do
|
57
|
+
subject(:workflow) { TestWorkflow.new(object) }
|
58
|
+
let(:object) { double(state: :test1) }
|
59
|
+
|
60
|
+
it 'errors on an invalid state' do
|
61
|
+
expect { workflow.transition to: :invalid }.to raise_error(ArgumentError)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'changes the state of the object' do
|
65
|
+
expect(object).to receive(:state=).with('test2')
|
66
|
+
workflow.transition to: :test2
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'leaves the state if transitions to itself' do
|
70
|
+
workflow.transition
|
71
|
+
expect(workflow.current_state_name).to eq(:test1)
|
72
|
+
expect(object.state).to eq(:test1)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'transition :after hooks' do
|
77
|
+
subject(:workflow) { TestWorkflow.new(object) }
|
78
|
+
let(:object) { OpenStruct.new(state: :test1, changes: {}, save: true, object_method: true) }
|
79
|
+
|
80
|
+
it 'does not call the :after hook before saving' do
|
81
|
+
expect(workflow).not_to receive(:workflow_method)
|
82
|
+
workflow.transition to: :test2, after: :workflow_method
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'does not calls the :after hook on failure' do
|
86
|
+
expect(workflow).not_to receive(:workflow_method)
|
87
|
+
workflow.transition to: :test1
|
88
|
+
workflow.save
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'calls the :after hook on a state method' do
|
92
|
+
expect_any_instance_of(Test1State).to receive(:state_method)
|
93
|
+
expect_any_instance_of(Test2State).not_to receive(:state_method)
|
94
|
+
workflow.transition to: :test2, after: :state_method
|
95
|
+
workflow.save
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'calls the :after hook on a workflow method' do
|
99
|
+
expect(workflow).to receive(:workflow_method)
|
100
|
+
workflow.transition to: :test2, after: :workflow_method
|
101
|
+
workflow.save
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'calls the :after hook on an object method' do
|
105
|
+
expect(object).to receive(:object_method)
|
106
|
+
workflow.transition to: :test2, after: :object_method
|
107
|
+
workflow.save
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'allows a lambda' do
|
111
|
+
expect_any_instance_of(Test1State).to receive(:state_method)
|
112
|
+
workflow.transition to: :test2, after: ->{ state_method }
|
113
|
+
workflow.save
|
114
|
+
end
|
115
|
+
|
116
|
+
describe 'after_transition hook' do
|
117
|
+
it 'calls the hook on transition' do
|
118
|
+
expect(workflow).to receive(:after_transition_callback)
|
119
|
+
workflow.transition to: :test2
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'does not call for invalid states' do
|
123
|
+
expect(workflow).not_to receive(:after_transition_callback)
|
124
|
+
# Will throw an ArgumentError for invalid state
|
125
|
+
expect { workflow.transition to: :invalid_state }.to raise_error
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'does not call for ending in the same state' do
|
129
|
+
expect(workflow).not_to receive(:after_transition_callback)
|
130
|
+
workflow.transition to: :test1
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe '#persist' do
|
136
|
+
subject(:workflow) { TestWorkflow.new(object) }
|
137
|
+
let(:object) { OpenStruct.new(state: :test1, changes: {}, save: true) }
|
138
|
+
|
139
|
+
context 'success' do
|
140
|
+
it 'saves the object' do
|
141
|
+
expect(workflow.persist).to be true
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'runs before and after_change hooks' do
|
145
|
+
expect(workflow.current_state).to receive(:fire_callbacks).with(:before_change, {})
|
146
|
+
expect(workflow.current_state).to receive(:fire_callbacks).with(:after_change, {})
|
147
|
+
expect(workflow.persist).to be true
|
148
|
+
end
|
149
|
+
|
150
|
+
it "runs after_enter hooks if it's in a new state" do
|
151
|
+
allow(object).to receive(:changes).and_return({'state' => ['test1', 'test2']})
|
152
|
+
expect(workflow.current_state).to receive(:fire_callbacks).with(:before_change, { 'state' => ['test1', 'test2'] })
|
153
|
+
expect(workflow.current_state).to receive(:fire_callbacks).with(:after_change, { 'state' => ['test1', 'test2'] })
|
154
|
+
expect(workflow.current_state).to receive(:fire_callbacks).with(:after_enter, { 'state' => ['test1', 'test2'] })
|
155
|
+
expect(workflow.persist).to be true
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'runs the before_save callback' do
|
159
|
+
expect(workflow).to receive(:before_save_callback)
|
160
|
+
workflow.persist
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'runs the after_save callback' do
|
164
|
+
expect(workflow).to receive(:after_save_callback)
|
165
|
+
workflow.persist
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
context 'failure' do
|
170
|
+
before do
|
171
|
+
expect(object).to receive(:save).and_return false
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'does not save the object' do
|
175
|
+
expect(workflow.persist).to be false
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'runs the before_change callback' do
|
179
|
+
expect(workflow.current_state).to receive(:fire_callbacks).with(:before_change, {})
|
180
|
+
expect(workflow.persist).to be false
|
181
|
+
end
|
182
|
+
|
183
|
+
it 'runs the before_save callback' do
|
184
|
+
expect(workflow).to receive(:before_save_callback)
|
185
|
+
workflow.persist
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'does not run the after_save callback' do
|
189
|
+
expect(workflow).to_not receive(:after_save_callback)
|
190
|
+
workflow.persist
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'reverts to the old state' do
|
194
|
+
expect(workflow).to be_test1
|
195
|
+
workflow.transition to: :test2
|
196
|
+
expect(workflow).to be_test2
|
197
|
+
workflow.persist
|
198
|
+
expect(workflow).to be_test1
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|