flow_machine 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|