flow_machine 0.2.2 → 0.2.3
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/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
|