flow_machine 0.2.2 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +8 -2
- data/Rakefile +13 -17
- data/lib/flow_machine.rb +1 -2
- data/lib/flow_machine/callback.rb +36 -33
- data/lib/flow_machine/change_callback.rb +10 -8
- data/lib/flow_machine/state_callback.rb +5 -3
- data/lib/flow_machine/version.rb +1 -1
- data/lib/flow_machine/workflow.rb +6 -6
- data/lib/flow_machine/workflow/factory_methods.rb +2 -1
- data/lib/flow_machine/workflow_state.rb +132 -122
- data/spec/flow_machine/factory_methods_spec.rb +23 -15
- data/spec/flow_machine/multiple_workflow_spec.rb +14 -8
- data/spec/flow_machine/workflow/model_extension_spec.rb +15 -15
- data/spec/flow_machine/workflow_callback_spec.rb +38 -34
- data/spec/flow_machine/workflow_change_callback_spec.rb +13 -10
- data/spec/flow_machine/workflow_spec.rb +55 -45
- data/spec/flow_machine/workflow_state/transition_callbacks_spec.rb +66 -0
- data/spec/flow_machine/workflow_state_spec.rb +100 -58
- data/spec/spec_helper.rb +7 -7
- metadata +22 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 07e50fbe9241a8d62738c46d08486aa3266540bdf16947f279e6468071381591
|
4
|
+
data.tar.gz: 19837451911696b6789209e65d6189ff65e7a01d05828297827339e3239d5e10
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a150dadcb0d9704686c57728c2c51aa7fe11ca349ca6a9462b1a38d3e05b0343b11babd41288733f791da1428a269556d406a026c323499ed5b346ee303af3b
|
7
|
+
data.tar.gz: b3dea74e0219d55b51f133a1aab01aac0b88f85a0ebf77e6a9849140c0dcfe443d81d430ec0a83da4dc607a1ec596a16859da71a5d6820daac74c0f4ff564472
|
data/README.md
CHANGED
@@ -43,10 +43,15 @@ end
|
|
43
43
|
|
44
44
|
class PublishedState < FlowMachine::WorkflowState
|
45
45
|
on_enter :notify_email_author
|
46
|
+
on_exit :clear_published_at
|
46
47
|
|
47
48
|
def notify_email_author
|
48
49
|
# Send an email
|
49
50
|
end
|
51
|
+
|
52
|
+
def clear_published_at
|
53
|
+
object.published_at = nil
|
54
|
+
end
|
50
55
|
end
|
51
56
|
```
|
52
57
|
|
@@ -114,7 +119,8 @@ State and Workflow callbacks accept `if` and `unless` options. They may be a sym
|
|
114
119
|
|
115
120
|
Declared in the `WorkflowState` class.
|
116
121
|
|
117
|
-
* `
|
122
|
+
* `on_exit` Called after the object has transitioned out of the state.
|
123
|
+
* `on_enter` Called after the object has transitioned into the state. Triggered after the previous state's `on_exit`.
|
118
124
|
|
119
125
|
The following are available when `Workflow#save` is used (`workflow.save` or `workflow.transition!`) *Not called if you call `save` directly on the decorated model*.
|
120
126
|
|
@@ -137,7 +143,7 @@ The following are available when `Workflow#save` is used:
|
|
137
143
|
|
138
144
|
Declared as an option to the `transition` method inside an `event` block.
|
139
145
|
|
140
|
-
* `after` Will be called after the transition has happened successfully. Useful when you only want something to trigger when moving from a specific state to another.
|
146
|
+
* `after` Will be called after the transition has happened successfully including persistance (if applicable). Useful when you only want something to trigger when moving from a specific state to another.
|
141
147
|
|
142
148
|
`transition to: :published, after: :send_mailing_list_email`
|
143
149
|
|
data/Rakefile
CHANGED
@@ -1,38 +1,34 @@
|
|
1
1
|
begin
|
2
|
-
require
|
2
|
+
require "bundler/setup"
|
3
3
|
rescue LoadError
|
4
|
-
puts
|
4
|
+
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
5
5
|
end
|
6
6
|
|
7
|
-
require
|
7
|
+
require "rdoc/task"
|
8
8
|
|
9
9
|
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
-
rdoc.rdoc_dir =
|
11
|
-
rdoc.title =
|
12
|
-
rdoc.options <<
|
13
|
-
rdoc.rdoc_files.include(
|
14
|
-
rdoc.rdoc_files.include(
|
10
|
+
rdoc.rdoc_dir = "rdoc"
|
11
|
+
rdoc.title = "Workflow"
|
12
|
+
rdoc.options << "--line-numbers"
|
13
|
+
rdoc.rdoc_files.include("README.rdoc")
|
14
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
15
15
|
end
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
17
|
Bundler::GemHelper.install_tasks
|
21
18
|
|
22
|
-
require
|
19
|
+
require "rake/testtask"
|
23
20
|
|
24
21
|
Rake::TestTask.new(:test) do |t|
|
25
|
-
t.libs <<
|
26
|
-
t.libs <<
|
27
|
-
t.pattern =
|
22
|
+
t.libs << "lib"
|
23
|
+
t.libs << "test"
|
24
|
+
t.pattern = "test/**/*_test.rb"
|
28
25
|
t.verbose = false
|
29
26
|
end
|
30
27
|
|
31
|
-
|
32
28
|
task default: :test
|
33
29
|
|
34
30
|
begin
|
35
|
-
require
|
31
|
+
require "rspec/core/rake_task"
|
36
32
|
RSpec::Core::RakeTask.new(:spec)
|
37
33
|
rescue LoadError
|
38
34
|
end
|
data/lib/flow_machine.rb
CHANGED
@@ -1,43 +1,46 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "active_support/core_ext/object/blank"
|
2
|
+
require "active_support/core_ext/array/extract_options"
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
4
|
+
module FlowMachine
|
5
|
+
class Callback
|
6
|
+
attr_accessor :method, :options
|
7
|
+
def initialize(*args, &block)
|
8
|
+
@options = args.extract_options!
|
9
|
+
@method = args.shift unless block
|
10
|
+
@block = block
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
call!(target)
|
15
|
-
end
|
13
|
+
def call(target, changes = {})
|
14
|
+
return unless will_run? target, changes
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
run_method_or_lambda(target, method.presence || @block)
|
20
|
-
end
|
16
|
+
call!(target)
|
17
|
+
end
|
21
18
|
|
22
|
-
|
23
|
-
|
24
|
-
target.
|
25
|
-
else
|
26
|
-
run_method(target, method)
|
19
|
+
# Runs the callback without any validations
|
20
|
+
def call!(target)
|
21
|
+
run_method_or_lambda(target, method.presence || @block)
|
27
22
|
end
|
28
|
-
end
|
29
23
|
|
30
|
-
|
31
|
-
|
32
|
-
|
24
|
+
def run_method_or_lambda(target, method)
|
25
|
+
if method.respond_to? :call # is it a lambda
|
26
|
+
target.instance_exec(&method)
|
27
|
+
else
|
28
|
+
run_method(target, method)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def run_method(target, method)
|
33
|
+
target.send(method)
|
34
|
+
end
|
33
35
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
36
|
+
def will_run?(target, _changes = {})
|
37
|
+
if options[:if]
|
38
|
+
[*options[:if]].all? { |m| run_method_or_lambda(target, m) }
|
39
|
+
elsif options[:unless]
|
40
|
+
[*options[:unless]].none? { |m| run_method_or_lambda(target, m) }
|
41
|
+
else
|
42
|
+
true
|
43
|
+
end
|
41
44
|
end
|
42
45
|
end
|
43
46
|
end
|
@@ -1,11 +1,13 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
1
|
+
module FlowMachine
|
2
|
+
class ChangeCallback < FlowMachine::StateCallback
|
3
|
+
attr_accessor :field
|
4
|
+
def initialize(field, *args, &block)
|
5
|
+
@field = field
|
6
|
+
super(*args, &block)
|
7
|
+
end
|
7
8
|
|
8
|
-
|
9
|
-
|
9
|
+
def will_run?(object, changes = {})
|
10
|
+
changes.key?(field.to_s) && super
|
11
|
+
end
|
10
12
|
end
|
11
13
|
end
|
@@ -1,5 +1,7 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
target
|
1
|
+
module FlowMachine
|
2
|
+
class StateCallback < FlowMachine::Callback
|
3
|
+
def run_method(target, method)
|
4
|
+
target.run_workflow_method(method)
|
5
|
+
end
|
4
6
|
end
|
5
7
|
end
|
data/lib/flow_machine/version.rb
CHANGED
@@ -14,11 +14,10 @@ module FlowMachine
|
|
14
14
|
end
|
15
15
|
|
16
16
|
module ClassMethods
|
17
|
-
|
18
17
|
attr_accessor :callbacks
|
19
18
|
|
20
19
|
def state_names
|
21
|
-
states.keys.map
|
20
|
+
states.keys.map(&:to_s)
|
22
21
|
end
|
23
22
|
|
24
23
|
def states
|
@@ -112,7 +111,7 @@ module FlowMachine
|
|
112
111
|
def current_state_name
|
113
112
|
object.send(self.class.state_method)
|
114
113
|
end
|
115
|
-
|
114
|
+
alias state current_state_name
|
116
115
|
|
117
116
|
def previous_state_name
|
118
117
|
@previous_state.try(:name)
|
@@ -139,7 +138,7 @@ module FlowMachine
|
|
139
138
|
self.changes = object.changes
|
140
139
|
# If the model has a default state from the database, then it doesn't get
|
141
140
|
# included in `changes` when you're first saving it.
|
142
|
-
|
141
|
+
changes[state_method.to_s] ||= [nil, current_state_name] if object.new_record?
|
143
142
|
|
144
143
|
fire_callbacks(:before_save)
|
145
144
|
current_state.fire_callbacks(:before_change, changes)
|
@@ -157,7 +156,7 @@ module FlowMachine
|
|
157
156
|
# Useful for using in if/unless on state and after_save callbacks so you can
|
158
157
|
# run the callback only on the initial persistence
|
159
158
|
def create?
|
160
|
-
|
159
|
+
changes[state_method.to_s].try(:first).blank?
|
161
160
|
end
|
162
161
|
|
163
162
|
def persist_object
|
@@ -165,7 +164,8 @@ module FlowMachine
|
|
165
164
|
end
|
166
165
|
|
167
166
|
def current_state_name=(new_state)
|
168
|
-
raise ArgumentError
|
167
|
+
raise ArgumentError, "invalid state: #{new_state}" unless self.class.state_names.include?(new_state.to_s)
|
168
|
+
|
169
169
|
object.send("#{self.class.state_method}=", new_state)
|
170
170
|
end
|
171
171
|
|
@@ -8,6 +8,7 @@ module FlowMachine
|
|
8
8
|
|
9
9
|
klazz = class_for(object)
|
10
10
|
return nil unless klazz
|
11
|
+
|
11
12
|
klazz.new(object, options)
|
12
13
|
end
|
13
14
|
|
@@ -19,7 +20,7 @@ module FlowMachine
|
|
19
20
|
if object_or_class.is_a? Class
|
20
21
|
"#{object_or_class.name}Workflow".constantize
|
21
22
|
else
|
22
|
-
|
23
|
+
class_for(object_or_class.class)
|
23
24
|
end
|
24
25
|
rescue NameError # if the workflow class doesn't exist
|
25
26
|
nil
|
@@ -1,162 +1,172 @@
|
|
1
1
|
require "active_support/core_ext/module/delegation"
|
2
2
|
require "active_support/inflector"
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
module FlowMachine
|
5
|
+
class WorkflowState
|
6
|
+
attr_reader :workflow
|
7
|
+
attr_accessor :guard_errors
|
7
8
|
|
8
|
-
|
9
|
+
delegate :object, :options, to: :workflow
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
module ClassMethods
|
12
|
+
attr_accessor :state_callbacks
|
13
|
+
attr_accessor :expose_to_workflow_methods
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
def state_name
|
16
|
+
name.demodulize.sub(/State\z/, "").underscore.to_sym
|
17
|
+
end
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
19
|
+
# Maintains a list of methods that should be exposed to the workflow
|
20
|
+
# the workflow is responsible for reading this list
|
21
|
+
def expose_to_workflow(name)
|
22
|
+
self.expose_to_workflow_methods ||= []
|
23
|
+
self.expose_to_workflow_methods << name
|
24
|
+
end
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
26
|
+
def event(name, options = {}, &block)
|
27
|
+
define_may_event(name, options)
|
28
|
+
define_event(name, options, &block)
|
29
|
+
define_event_bang(name)
|
30
|
+
end
|
30
31
|
|
31
|
-
|
32
|
+
private
|
33
|
+
|
34
|
+
def define_event(name, _options, &block)
|
35
|
+
define_method name do |*args|
|
36
|
+
return false unless send("may_#{name}?")
|
32
37
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
instance_exec *args, &block
|
38
|
+
instance_exec(*args, &block)
|
39
|
+
end
|
40
|
+
expose_to_workflow name
|
37
41
|
end
|
38
|
-
expose_to_workflow name
|
39
|
-
end
|
40
42
|
|
41
|
-
|
42
|
-
|
43
|
-
|
43
|
+
def define_may_event(name, options)
|
44
|
+
define_method "may_#{name}?" do
|
45
|
+
run_guard_methods([*options[:guard]])
|
46
|
+
end
|
47
|
+
expose_to_workflow "may_#{name}?"
|
44
48
|
end
|
45
|
-
expose_to_workflow "may_#{name}?"
|
46
|
-
end
|
47
49
|
|
48
|
-
|
49
|
-
|
50
|
-
|
50
|
+
def define_event_bang(name)
|
51
|
+
define_method "#{name}!" do |*args|
|
52
|
+
workflow.persist if send(name, *args)
|
53
|
+
end
|
54
|
+
expose_to_workflow "#{name}!"
|
51
55
|
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
56
|
end
|
57
|
+
extend ClassMethods
|
58
|
+
|
59
|
+
# Callbacks may be a symbol method name on the state, workflow, or underlying object,
|
60
|
+
# and will look for that method on those objects in that order. You may also
|
61
|
+
# use a block.
|
62
|
+
# Callbacks will accept :if and :unless options, which also may be method name
|
63
|
+
# symbols or blocks. The option accepts an array meaning all methods must return
|
64
|
+
# true (for if) and false (for unless)
|
65
|
+
#
|
66
|
+
# class ExampleState < Workflow::State
|
67
|
+
# on_enter :some_method, if: :allowed?
|
68
|
+
# after_enter :after_enter_method, if: [:this_is_true?, :and_this_is_true?]
|
69
|
+
# before_change(:field_name) { do_something }
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
module CallbackDsl
|
73
|
+
# Called when the workflow `transition`s to the state
|
74
|
+
def on_enter(*args, &block)
|
75
|
+
add_callback(:on_enter, FlowMachine::StateCallback.new(*args, &block))
|
76
|
+
end
|
75
77
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
78
|
+
# Called after `persist` when the workflow transitioned into this state
|
79
|
+
def after_enter(*args, &block)
|
80
|
+
add_callback(:after_enter, FlowMachine::StateCallback.new(*args, &block))
|
81
|
+
end
|
80
82
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
83
|
+
# Called when the worklow `transition`s out of the state
|
84
|
+
def on_exit(*args, &block)
|
85
|
+
add_callback(:on_exit, FlowMachine::StateCallback.new(*args, &block))
|
86
|
+
end
|
87
|
+
|
88
|
+
# Happens before persistence if the field on the object has changed
|
89
|
+
def before_change(field, *args, &block)
|
90
|
+
add_callback(:before_change, FlowMachine::ChangeCallback.new(field, *args, &block))
|
91
|
+
end
|
92
|
+
|
93
|
+
# Happens after persistence if the field on the object has changed
|
94
|
+
def after_change(field, *args, &block)
|
95
|
+
add_callback(:after_change, FlowMachine::ChangeCallback.new(field, *args, &block))
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
85
99
|
|
86
|
-
|
87
|
-
|
88
|
-
|
100
|
+
def add_callback(hook, callback)
|
101
|
+
self.state_callbacks ||= {}
|
102
|
+
state_callbacks[hook] ||= []
|
103
|
+
state_callbacks[hook] << callback
|
104
|
+
end
|
89
105
|
end
|
106
|
+
extend CallbackDsl
|
90
107
|
|
91
|
-
|
108
|
+
def initialize(workflow)
|
109
|
+
@workflow = workflow
|
110
|
+
@guard_errors = []
|
111
|
+
end
|
92
112
|
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
|
113
|
+
def fire_callback_list(callbacks, changes = {})
|
114
|
+
callbacks.each do |callback|
|
115
|
+
callback.call(self, changes)
|
116
|
+
end
|
97
117
|
end
|
98
|
-
end
|
99
|
-
extend CallbackDsl
|
100
118
|
|
101
|
-
|
102
|
-
|
103
|
-
@guard_errors = []
|
104
|
-
end
|
119
|
+
def fire_callbacks(event, changes = {})
|
120
|
+
return unless self.class.state_callbacks.try(:[], event)
|
105
121
|
|
106
|
-
|
107
|
-
callbacks.each do |callback|
|
108
|
-
callback.call(self, changes)
|
122
|
+
fire_callback_list self.class.state_callbacks[event], changes
|
109
123
|
end
|
110
|
-
end
|
111
124
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
125
|
+
# Allows method calls to fallback up the object chain so
|
126
|
+
# guards and other methods can be defined on the object or workflow
|
127
|
+
# as well as the state
|
128
|
+
def run_workflow_method(method_name, *args, &block)
|
129
|
+
target = object_chain(method_name)
|
130
|
+
raise NoMethodError.new("undefined method #{method_name}", method_name) unless target
|
116
131
|
|
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
132
|
target.send(method_name, *args, &block)
|
123
|
-
else
|
124
|
-
raise NoMethodError.new("undefined method #{method_name}", method_name)
|
125
133
|
end
|
126
|
-
end
|
127
134
|
|
128
|
-
|
129
|
-
|
130
|
-
|
135
|
+
def transition(options = {})
|
136
|
+
workflow.transition(options).tap do |new_state|
|
137
|
+
if new_state != workflow.previous_state
|
138
|
+
workflow.previous_state.fire_callbacks(:on_exit)
|
139
|
+
new_state.fire_callbacks(:on_enter)
|
140
|
+
end
|
141
|
+
end
|
131
142
|
end
|
132
|
-
end
|
133
143
|
|
134
|
-
|
135
|
-
|
136
|
-
|
144
|
+
def name
|
145
|
+
self.class.state_name
|
146
|
+
end
|
137
147
|
|
138
|
-
|
139
|
-
|
140
|
-
|
148
|
+
def ==(other)
|
149
|
+
self.class == other.class
|
150
|
+
end
|
141
151
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
152
|
+
private
|
153
|
+
|
154
|
+
def run_guard_methods(guard_methods)
|
155
|
+
self.guard_errors = []
|
156
|
+
# Use inject to ensure that all guard methods are run.
|
157
|
+
# all? short circuits on first false value
|
158
|
+
guard_methods.inject(true) do |valid, guard_method|
|
159
|
+
if run_workflow_method(guard_method)
|
160
|
+
valid
|
161
|
+
else
|
162
|
+
guard_errors << guard_method
|
163
|
+
false
|
164
|
+
end
|
154
165
|
end
|
155
166
|
end
|
156
|
-
#
|
157
|
-
end
|
158
167
|
|
159
|
-
|
160
|
-
|
168
|
+
def object_chain(method_name)
|
169
|
+
[self, workflow, object].find { |o| o.respond_to?(method_name, true) }
|
170
|
+
end
|
161
171
|
end
|
162
172
|
end
|