business_flow 0.7.0 → 0.8.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 +4 -4
- data/Gemfile.lock +1 -1
- data/lib/business_flow/base.rb +1 -32
- data/lib/business_flow/cacheable.rb +24 -26
- data/lib/business_flow/callable.rb +59 -33
- data/lib/business_flow/default_step_executor.rb +6 -33
- data/lib/business_flow/dsl.rb +227 -134
- data/lib/business_flow/instrument.rb +17 -0
- data/lib/business_flow/instrumented_executor.rb +28 -0
- data/lib/business_flow/instrumented_step_executor.rb +23 -0
- data/lib/business_flow/step.rb +54 -13
- data/lib/business_flow/validations.rb +25 -0
- data/lib/business_flow/version.rb +1 -1
- data/lib/business_flow.rb +4 -1
- metadata +6 -3
- data/lib/business_flow/not_nil_validator.rb +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 430484fefee99d40a5f0043d018409179459cf87
|
4
|
+
data.tar.gz: 28aa9f6aabb03d731824498e96c00d7bb23aa224
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2bb43af5b2ff636c7e1cd78cf6b6b0e5c4211969c5edac41d999a75f6e3f768921053dd6c4c4acd8119de9f61a290636095fc784fbc3af19a188ccc1577d2d9d
|
7
|
+
data.tar.gz: 775da5c4e2a71d925e7963d34ec97e78773fd69bf057e85f476b578788740ea7bf7f25832bd0ff9b89a9f1336ee60f114b208e9ef1071bf620ef99717df87cb0
|
data/Gemfile.lock
CHANGED
data/lib/business_flow/base.rb
CHANGED
@@ -1,40 +1,9 @@
|
|
1
1
|
module BusinessFlow
|
2
2
|
# include BusinessFlow::Base in any object to get a flow!
|
3
|
-
# :reek:ModuleInitialize
|
4
3
|
module Base
|
5
4
|
def self.included(klass)
|
6
5
|
klass.include(DSL)
|
7
|
-
klass.
|
8
|
-
end
|
9
|
-
|
10
|
-
# "Helper" methods which we put on an object to provide some default
|
11
|
-
# behavior.
|
12
|
-
module InstanceMethods
|
13
|
-
attr_reader :parameter_object
|
14
|
-
private :parameter_object
|
15
|
-
|
16
|
-
# Initialize is here so that we can set the parameter object, and then
|
17
|
-
# allow our "parent" class to handle any additional initialization that
|
18
|
-
# it needs to do.
|
19
|
-
def initialize(parameter_object)
|
20
|
-
@parameter_object = parameter_object
|
21
|
-
catch(:halt_step) do
|
22
|
-
super()
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def call
|
27
|
-
# If our initialization process set any errors, return
|
28
|
-
return if errors.any? || invalid?
|
29
|
-
if defined?(super)
|
30
|
-
catch(:halt_step) do
|
31
|
-
super
|
32
|
-
end
|
33
|
-
else
|
34
|
-
::BusinessFlow::DefaultStepExecutor.new(self.class.step_queue, self)
|
35
|
-
.call
|
36
|
-
end
|
37
|
-
end
|
6
|
+
klass.include(Validations)
|
38
7
|
end
|
39
8
|
end
|
40
9
|
end
|
@@ -13,6 +13,21 @@ module BusinessFlow
|
|
13
13
|
|
14
14
|
# DSL Methods
|
15
15
|
module ClassMethods
|
16
|
+
# Responsible for converting our DSL options into cache store options
|
17
|
+
CacheOptions = Struct.new(:ttl) do
|
18
|
+
def to_store_options
|
19
|
+
# compact is not available in Ruby <2.4 or ActiveSupport < 4, so
|
20
|
+
# we can't use it here.
|
21
|
+
options = {}
|
22
|
+
options[:expires_in] = ttl if ttl
|
23
|
+
options
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def cache_options
|
28
|
+
@cache_options ||= CacheOptions.new
|
29
|
+
end
|
30
|
+
|
16
31
|
def cache_store(store = nil)
|
17
32
|
if store
|
18
33
|
@cache_store = store
|
@@ -27,45 +42,28 @@ module BusinessFlow
|
|
27
42
|
|
28
43
|
def cache_ttl(ttl = nil)
|
29
44
|
if ttl
|
30
|
-
|
45
|
+
cache_options.ttl = ttl
|
31
46
|
else
|
32
|
-
|
47
|
+
cache_options.ttl
|
33
48
|
end
|
34
49
|
end
|
35
50
|
|
36
51
|
def cache_key(key = nil)
|
37
52
|
if key
|
38
|
-
@cache_key = Callable.new(key
|
53
|
+
@cache_key = Callable.new(key)
|
39
54
|
else
|
40
|
-
@cache_key ||= Callable.new(:parameter_object
|
55
|
+
@cache_key ||= Callable.new(:parameter_object)
|
41
56
|
end
|
42
57
|
end
|
43
58
|
|
44
|
-
def
|
45
|
-
flow
|
46
|
-
|
47
|
-
rescue FlowFailedException
|
48
|
-
flow
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
# Avoid polluting the namespace of whoever includes us
|
53
|
-
module PrivateHelpers
|
54
|
-
def self.execute(klass, flow)
|
55
|
-
klass.cache_store.fetch(flow.cache_key, cache_options(klass)) do
|
56
|
-
flow.call
|
59
|
+
def execute(flow)
|
60
|
+
cache_store.fetch(flow.cache_key, cache_options.to_store_options) do
|
61
|
+
super(flow)
|
57
62
|
raise FlowFailedException, flow if flow.errors.any?
|
58
63
|
flow
|
59
64
|
end
|
60
|
-
|
61
|
-
|
62
|
-
def self.cache_options(klass)
|
63
|
-
# compact is not available in Ruby <2.4 or ActiveSupport < 4, so
|
64
|
-
# we can't use it here.
|
65
|
-
options = {}
|
66
|
-
ttl = klass.cache_ttl
|
67
|
-
options[:expires_in] = ttl if ttl
|
68
|
-
options
|
65
|
+
rescue FlowFailedException
|
66
|
+
flow
|
69
67
|
end
|
70
68
|
end
|
71
69
|
end
|
@@ -3,14 +3,21 @@ module BusinessFlow
|
|
3
3
|
# method, a symbol representing another class which implements .call, or a
|
4
4
|
# proc/lambda
|
5
5
|
class Callable
|
6
|
-
def initialize(callable
|
7
|
-
@metaclass = metaclass
|
6
|
+
def initialize(callable)
|
8
7
|
@callable = callable
|
9
8
|
check_callable
|
10
9
|
end
|
11
10
|
|
11
|
+
# :reek:ManualDispatch
|
12
12
|
def call(instance, inputs)
|
13
|
-
|
13
|
+
if instance.respond_to?(@callable, true)
|
14
|
+
send_callable(instance, inputs)
|
15
|
+
else
|
16
|
+
@callable = lookup_callable(instance) ||
|
17
|
+
raise(NameError, "undefined constant #{@callable}")
|
18
|
+
check_callable
|
19
|
+
call(instance, inputs)
|
20
|
+
end
|
14
21
|
end
|
15
22
|
|
16
23
|
def to_s
|
@@ -19,55 +26,74 @@ module BusinessFlow
|
|
19
26
|
|
20
27
|
private
|
21
28
|
|
22
|
-
|
29
|
+
def send_callable(instance, inputs)
|
30
|
+
instance_eval %{
|
31
|
+
def call(instance, _inputs)
|
32
|
+
instance.send(@callable)
|
33
|
+
end
|
34
|
+
}, __FILE__, __LINE__ - 4
|
35
|
+
call(instance, inputs)
|
36
|
+
end
|
37
|
+
|
23
38
|
def check_callable
|
24
39
|
if @callable.is_a?(Proc)
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
40
|
+
proc_callable
|
41
|
+
else
|
42
|
+
call_callable
|
43
|
+
end
|
44
|
+
rescue NameError
|
45
|
+
unless @callable.is_a?(Symbol)
|
29
46
|
raise ArgumentError, 'callable must be a symbol or respond to #call'
|
30
47
|
end
|
31
48
|
end
|
32
49
|
|
33
|
-
def
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
@callable = lookup_callable ||
|
40
|
-
raise(NameError, "undefined constant #{@callable}")
|
41
|
-
check_callable
|
50
|
+
def proc_callable
|
51
|
+
instance_eval %{
|
52
|
+
def call(instance, inputs)
|
53
|
+
instance.instance_exec(
|
54
|
+
#{@callable.arity.zero? ? '' : 'inputs, '}&@callable
|
55
|
+
)
|
42
56
|
end
|
57
|
+
}, __FILE__, __LINE__ - 6
|
43
58
|
end
|
44
59
|
|
45
|
-
def
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
60
|
+
def call_callable
|
61
|
+
case @callable.method(:call).arity
|
62
|
+
when 1, -1
|
63
|
+
single_inputs_callable
|
64
|
+
when 0
|
65
|
+
zero_inputs_callable
|
66
|
+
else two_inputs_callable
|
52
67
|
end
|
53
68
|
end
|
54
69
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
when 1, -1
|
70
|
+
def single_inputs_callable
|
71
|
+
instance_eval %{
|
72
|
+
def call(_instance, inputs)
|
59
73
|
@callable.call(inputs)
|
60
|
-
|
74
|
+
end
|
75
|
+
}, __FILE__, __LINE__ - 4
|
76
|
+
end
|
77
|
+
|
78
|
+
def zero_inputs_callable
|
79
|
+
instance_eval %{
|
80
|
+
def call(_instance, _inputs)
|
61
81
|
@callable.call
|
62
|
-
|
82
|
+
end
|
83
|
+
}, __FILE__, __LINE__ - 4
|
84
|
+
end
|
85
|
+
|
86
|
+
def two_inputs_callable
|
87
|
+
instance_eval %{
|
88
|
+
def call(instance, inputs)
|
63
89
|
@callable.call(instance, inputs)
|
64
90
|
end
|
65
|
-
|
91
|
+
}, __FILE__, __LINE__ - 4
|
66
92
|
end
|
67
93
|
|
68
|
-
def lookup_callable
|
94
|
+
def lookup_callable(first_instance)
|
69
95
|
constant_name = @callable.to_s.camelcase
|
70
|
-
|
96
|
+
first_instance.class.parents.each do |parent|
|
71
97
|
begin
|
72
98
|
return parent.const_get(constant_name)
|
73
99
|
rescue NameError
|
@@ -9,49 +9,22 @@ module BusinessFlow
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def call
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
break unless process_step(step)
|
16
|
-
end
|
12
|
+
@step_queue.each do |step|
|
13
|
+
break if @flow.errors?
|
14
|
+
execute_step(step)
|
17
15
|
end
|
18
16
|
end
|
19
17
|
|
20
18
|
protected
|
21
19
|
|
22
|
-
|
23
|
-
catch(:halt_step) { execute_step(step) }
|
24
|
-
return true if @flow.errors.blank?
|
25
|
-
ActiveSupport::Notifications.publish(
|
26
|
-
event_name(step) + '.error', step: step, flow: @flow
|
27
|
-
)
|
28
|
-
false
|
29
|
-
end
|
20
|
+
attr_reader :flow, :step_queue
|
30
21
|
|
31
22
|
def execute_step(step)
|
32
|
-
|
33
|
-
|
34
|
-
) do |payload|
|
35
|
-
payload[:step_result] = result = step.call(@flow)
|
23
|
+
catch(:halt_step) do
|
24
|
+
result = step.call(@flow)
|
36
25
|
result.merge_into(@flow)
|
37
26
|
result
|
38
27
|
end
|
39
28
|
end
|
40
|
-
|
41
|
-
def flow_name
|
42
|
-
@flow_name ||= @flow.class.to_s.underscore
|
43
|
-
end
|
44
|
-
|
45
|
-
def flow_event_name
|
46
|
-
@flow_event_name ||= "business_flow.flow.#{flow_name}"
|
47
|
-
end
|
48
|
-
|
49
|
-
def step_event_name(step)
|
50
|
-
"#{flow_name}.#{step.to_s.underscore}"
|
51
|
-
end
|
52
|
-
|
53
|
-
def event_name(step)
|
54
|
-
"business_flow.step.#{step_event_name(step)}"
|
55
|
-
end
|
56
29
|
end
|
57
30
|
end
|
data/lib/business_flow/dsl.rb
CHANGED
@@ -3,10 +3,6 @@ module BusinessFlow
|
|
3
3
|
# ClassMethods.
|
4
4
|
module DSL
|
5
5
|
# Contains the DSL for BusinessFlow
|
6
|
-
# The class that includes this must implement a parameter_object reader
|
7
|
-
# which returns a hash or object representing the parameters the flow
|
8
|
-
# was initialized with. The provided .call will instantiate the including
|
9
|
-
# class with a parameter_object as the only argument.
|
10
6
|
module ClassMethods
|
11
7
|
# Requires that a field be retrievable from the initialization parameters
|
12
8
|
#
|
@@ -15,200 +11,297 @@ module BusinessFlow
|
|
15
11
|
#
|
16
12
|
# @param fields The fields required from the initialization parameters
|
17
13
|
def needs(*fields)
|
18
|
-
@needs ||= []
|
19
|
-
|
20
|
-
@needs.push(*fields)
|
21
|
-
fields.each do |field|
|
22
|
-
PrivateHelpers.create_parameter_field(self, field)
|
23
|
-
end
|
14
|
+
@needs ||= FieldList.new([], ParameterField, self)
|
15
|
+
@needs.add_fields(fields)
|
24
16
|
end
|
25
17
|
|
26
18
|
# Allows a field to be retrieved from the initialiaztion parameters
|
27
19
|
def wants(field, default = proc { nil }, opts = {})
|
28
20
|
internal_name = "wants_#{field}".to_sym
|
29
21
|
uses(internal_name, default, opts)
|
30
|
-
|
22
|
+
ParameterField.new(field, internal_name).add_to(self)
|
31
23
|
end
|
32
24
|
|
33
25
|
# Declares that you will expose a field to the outside world.
|
34
26
|
def provides(*fields)
|
35
|
-
@provides ||= []
|
36
|
-
|
37
|
-
@provides.push(*fields)
|
38
|
-
fields.each { |field| PrivateHelpers.create_field(self, field) }
|
39
|
-
end
|
40
|
-
|
41
|
-
# Declares that you expect to set this field during the course of
|
42
|
-
# processing, and that it should meet the given ActiveModel
|
43
|
-
# validations.
|
44
|
-
def expects(field, options = {})
|
45
|
-
validates field, options.merge(on: field)
|
46
|
-
PrivateHelpers.create_field(self, field)
|
27
|
+
@provides ||= FieldList.new([], PublicField, self)
|
28
|
+
@provides.add_fields(fields)
|
47
29
|
end
|
48
30
|
|
49
31
|
def uses(field, klass, opts = {})
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
private field
|
32
|
+
step = Step.new(Callable.new(klass), opts)
|
33
|
+
retriever = proc { step.call(self).output }
|
34
|
+
UsesField.new(field, retriever).add_to(self)
|
54
35
|
end
|
55
36
|
|
56
37
|
def step(klass, opts = {})
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
)
|
61
|
-
step_queue << step = Step.new(callable, opts)
|
62
|
-
step.outputs.values.each do |field|
|
63
|
-
PrivateHelpers.create_field(self, field)
|
64
|
-
end
|
38
|
+
step = Step.new(Callable.new(klass), opts)
|
39
|
+
step_queue.push(step)
|
40
|
+
step.outputs.values.each { |field| Field.new(field).add_to(self) }
|
65
41
|
end
|
66
42
|
|
67
43
|
def call(parameter_object)
|
68
|
-
|
44
|
+
allocate.tap do |flow|
|
45
|
+
catch(:halt_step) do
|
46
|
+
flow.send(:_business_flow_dsl_initialize,
|
47
|
+
ParameterObject.new(parameter_object), needs)
|
48
|
+
execute(flow)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# :reek:UtilityFunction
|
54
|
+
def execute(flow)
|
55
|
+
flow.call
|
56
|
+
end
|
57
|
+
|
58
|
+
def build(parameter_object)
|
59
|
+
call(parameter_object)
|
69
60
|
end
|
70
61
|
|
71
62
|
def call!(*args)
|
72
|
-
|
73
|
-
raise FlowFailedException,
|
74
|
-
|
63
|
+
flow = call(*args)
|
64
|
+
raise FlowFailedException, flow if flow.errors.any?
|
65
|
+
flow
|
75
66
|
end
|
76
67
|
|
77
68
|
def step_queue
|
78
69
|
@step_queue ||= []
|
79
70
|
end
|
71
|
+
|
72
|
+
def step_executor(executor_class = nil)
|
73
|
+
if executor_class
|
74
|
+
@executor_class = executor_class
|
75
|
+
else
|
76
|
+
@executor_class ||= ::BusinessFlow::DefaultStepExecutor
|
77
|
+
end
|
78
|
+
end
|
80
79
|
end
|
81
80
|
|
82
81
|
def self.included(klass)
|
83
|
-
# That we include ActiveModel::Validations is considered part of our
|
84
|
-
# public API, even though we provide our own aliases.
|
85
|
-
klass.include(ActiveModel::Validations)
|
86
82
|
klass.extend(ClassMethods)
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
83
|
+
end
|
84
|
+
|
85
|
+
attr_reader :parameter_object
|
86
|
+
private :parameter_object
|
87
|
+
|
88
|
+
def call
|
89
|
+
return if invalid?
|
90
|
+
klass = self.class
|
91
|
+
klass.step_executor.new(klass.step_queue, self).call
|
92
|
+
end
|
93
|
+
|
94
|
+
# Responsible for setting the parameter object and validating inputs.
|
95
|
+
# This is a method directly on the object instead of something we
|
96
|
+
# handle through instance_eval/exec for performance reasons.
|
97
|
+
# :reek:NilCheck
|
98
|
+
private def _business_flow_dsl_initialize(parameter_object, needs)
|
99
|
+
@parameter_object = parameter_object
|
100
|
+
needs.each do |need|
|
101
|
+
if send(need).nil?
|
102
|
+
errors[need] << 'must not be nil'
|
103
|
+
throw :halt_step
|
91
104
|
end
|
92
|
-
validates_with NotNilValidator
|
93
105
|
end
|
106
|
+
initialize
|
94
107
|
end
|
95
108
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
# Handle some logic around conditions
|
100
|
-
class ConditionList
|
101
|
-
def initialize(if_stmts, unless_stmts, klass)
|
102
|
-
@klass = klass
|
103
|
-
@conditions = Array.wrap(if_stmts).map(&method(:to_if)) +
|
104
|
-
Array.wrap(unless_stmts).map(&method(:to_unless))
|
105
|
-
end
|
109
|
+
def errors
|
110
|
+
@errors ||= ActiveModel::Errors.new(self)
|
111
|
+
end
|
106
112
|
|
107
|
-
|
108
|
-
|
109
|
-
|
113
|
+
def errors?
|
114
|
+
@errors.present?
|
115
|
+
end
|
110
116
|
|
111
|
-
|
117
|
+
def valid?(_context = nil)
|
118
|
+
@errors.blank?
|
119
|
+
end
|
112
120
|
|
113
|
-
|
114
|
-
|
115
|
-
|
121
|
+
def invalid?(context = nil)
|
122
|
+
!valid?(context)
|
123
|
+
end
|
116
124
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
125
|
+
# Responsible for creating fields on a class and noting the of field
|
126
|
+
class FieldList
|
127
|
+
attr_reader :field_list
|
128
|
+
|
129
|
+
def initialize(field_list, field_klass, klass)
|
130
|
+
@field_list = field_list
|
131
|
+
@field_klass = field_klass
|
132
|
+
@klass = klass
|
124
133
|
end
|
125
134
|
|
126
|
-
def
|
127
|
-
|
128
|
-
|
135
|
+
def add_fields(fields)
|
136
|
+
fields.each { |field| @field_klass.new(field).add_to(@klass) }
|
137
|
+
@field_list.concat(fields)
|
129
138
|
end
|
139
|
+
end
|
130
140
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
141
|
+
# Helper class to manage logic around adding fields
|
142
|
+
class Field
|
143
|
+
def initialize(field)
|
144
|
+
@field = field
|
145
|
+
# For proc bindings.
|
146
|
+
ivar_name = instance_variable_name
|
147
|
+
@getter = ivar_name
|
148
|
+
@setter = self.class.setter_factory(field, ivar_name)
|
149
|
+
end
|
150
|
+
|
151
|
+
def add_to(klass)
|
152
|
+
Field.eval_method(klass, field, getter)
|
153
|
+
Field.eval_method(klass, setter_name, setter)
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.eval_method(klass, name, str)
|
157
|
+
return if klass.method_defined?(name) ||
|
158
|
+
klass.private_method_defined?(name)
|
159
|
+
unsafe_eval_method(klass, name, str)
|
160
|
+
end
|
161
|
+
|
162
|
+
def self.unsafe_eval_method(klass, name, str)
|
163
|
+
body = ["private def #{name}", str, 'end'].join("\n")
|
164
|
+
klass.class_eval body, __FILE__, __LINE__
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.setter_factory(field, ivar_name)
|
168
|
+
<<-SETTER
|
169
|
+
#{ivar_name} = new_value
|
170
|
+
throw :halt_step unless valid?(:#{field})
|
171
|
+
new_value
|
172
|
+
SETTER
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
attr_reader :field, :getter, :setter
|
178
|
+
|
179
|
+
def setter_name
|
180
|
+
@setter_name ||= "#{field}=(new_value)"
|
181
|
+
end
|
182
|
+
|
183
|
+
def instance_variable_name
|
184
|
+
@instance_variable_name ||= "@#{field}"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Create a field with a public getter
|
189
|
+
class PublicField
|
190
|
+
def initialize(field)
|
191
|
+
@name = field
|
192
|
+
@field = Field.new(field)
|
193
|
+
end
|
194
|
+
|
195
|
+
def add_to(klass)
|
196
|
+
@field.add_to(klass)
|
197
|
+
klass.send(:public, @name)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Helper class around memoized fields
|
202
|
+
class MemoizedField
|
203
|
+
def initialize(field, retriever, setter_factory)
|
204
|
+
@field = field
|
205
|
+
@retriever = retriever
|
206
|
+
@setter_factory = setter_factory
|
207
|
+
end
|
208
|
+
|
209
|
+
def add_to(klass)
|
210
|
+
setter = setter_factory.call(field, safe_ivar_name)
|
211
|
+
Field.unsafe_eval_method(
|
212
|
+
klass, field, memoized(safe_ivar_name, setter, retriever)
|
213
|
+
)
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
attr_reader :field, :retriever, :setter_factory
|
219
|
+
|
220
|
+
def memoized(ivar_name, setter, retriever)
|
221
|
+
<<-MEMOIZED
|
222
|
+
return #{ivar_name} if defined?(#{ivar_name})
|
223
|
+
new_value = begin
|
224
|
+
#{retriever}
|
137
225
|
end
|
138
|
-
|
226
|
+
#{setter}
|
227
|
+
MEMOIZED
|
139
228
|
end
|
140
229
|
|
141
|
-
def
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
nil
|
148
|
-
end || (fallback && send(fallback))
|
230
|
+
def safe_ivar_name
|
231
|
+
@safe_ivar_name ||= begin
|
232
|
+
"@business_flow_dsl_#{field}"
|
233
|
+
.sub(/\?$/, '_query')
|
234
|
+
.sub(/\!$/, '_bang')
|
235
|
+
.to_sym
|
149
236
|
end
|
150
237
|
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# Responsible for declaring fields which will be memoized and validated
|
241
|
+
# when first set
|
242
|
+
class UsesField
|
243
|
+
def initialize(field, retriever)
|
244
|
+
@name = field
|
245
|
+
@retriever = retriever
|
246
|
+
@field = MemoizedField.new(field, retriever_method_name,
|
247
|
+
Field.method(:setter_factory))
|
248
|
+
end
|
151
249
|
|
152
|
-
def
|
153
|
-
|
154
|
-
|
250
|
+
def add_to(klass)
|
251
|
+
klass.send(:define_method, retriever_method_name, &@retriever)
|
252
|
+
klass.send(:private, retriever_method_name)
|
253
|
+
@field.add_to(klass)
|
155
254
|
end
|
156
255
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
256
|
+
private
|
257
|
+
|
258
|
+
def retriever_method_name
|
259
|
+
@retriever_method_name ||=
|
260
|
+
"_business_flow_dsl_execute_step_for_#{@name}".to_sym
|
161
261
|
end
|
262
|
+
end
|
162
263
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
264
|
+
# Helper class around input parameter fields
|
265
|
+
class ParameterField
|
266
|
+
def initialize(field, fallback = nil)
|
267
|
+
retriever = "@parameter_object.fetch(:#{field})"
|
268
|
+
retriever += " { send(:#{fallback}) }" if fallback
|
269
|
+
@field = MemoizedField.new(field, retriever, method(:setter_factory))
|
169
270
|
end
|
170
271
|
|
171
|
-
def
|
172
|
-
|
173
|
-
klass.private_method_defined?(field)
|
174
|
-
klass.send(:attr_reader, field)
|
272
|
+
def add_to(klass)
|
273
|
+
@field.add_to(klass)
|
175
274
|
end
|
176
275
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
&setter_proc("@#{field}", field))
|
276
|
+
private
|
277
|
+
|
278
|
+
def setter_factory(_field, ivar_name)
|
279
|
+
"#{ivar_name} = new_value"
|
182
280
|
end
|
281
|
+
end
|
183
282
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
elsif ivar_name.end_with?('!')
|
189
|
-
ivar_name.sub!(/\!$/, '_bang')
|
190
|
-
end
|
191
|
-
ivar_name.to_sym
|
283
|
+
# Manage logic around input parameters
|
284
|
+
class ParameterObject
|
285
|
+
def initialize(parameters)
|
286
|
+
@parameters = parameters
|
192
287
|
end
|
193
288
|
|
194
|
-
def
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
new_value
|
199
|
-
end
|
289
|
+
def fetch(key)
|
290
|
+
value = inner_fetch(key)
|
291
|
+
return yield if !value && block_given?
|
292
|
+
value
|
200
293
|
end
|
201
294
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
instance_exec(step.call(self).output, &setter_proc)
|
210
|
-
end
|
295
|
+
private
|
296
|
+
|
297
|
+
def inner_fetch(key)
|
298
|
+
if @parameters.is_a?(Hash) && @parameters.key?(key)
|
299
|
+
@parameters[key]
|
300
|
+
else
|
301
|
+
@parameters.public_send(key)
|
211
302
|
end
|
303
|
+
rescue NoMethodError
|
304
|
+
nil
|
212
305
|
end
|
213
306
|
end
|
214
307
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module BusinessFlow
|
2
|
+
# Include me to fire ActiveSupport notifications on the flow, every step,
|
3
|
+
# and for any errors that happen.
|
4
|
+
module Instrument
|
5
|
+
def self.included(klass)
|
6
|
+
klass.extend(ClassMethods)
|
7
|
+
klass.step_executor ::BusinessFlow::InstrumentedExecutor
|
8
|
+
end
|
9
|
+
|
10
|
+
# Contains methods that we add to the DSL
|
11
|
+
module ClassMethods
|
12
|
+
def instrument_steps
|
13
|
+
step_executor ::BusinessFlow::InstrumentedStepExecutor
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module BusinessFlow
|
2
|
+
# Fire ActiveSupport events for every step that's run and on errors
|
3
|
+
class InstrumentedExecutor < DefaultStepExecutor
|
4
|
+
def call
|
5
|
+
name = flow_event_name
|
6
|
+
payload = { flow: flow }
|
7
|
+
ActiveSupport::Notifications.instrument(name, payload) do
|
8
|
+
super
|
9
|
+
end
|
10
|
+
notify_errors(name, payload)
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def notify_errors(name, payload)
|
16
|
+
return unless flow.errors?
|
17
|
+
ActiveSupport::Notifications.publish(name + '.error', payload)
|
18
|
+
end
|
19
|
+
|
20
|
+
def flow_name
|
21
|
+
@flow_name ||= flow.class.to_s.underscore
|
22
|
+
end
|
23
|
+
|
24
|
+
def flow_event_name
|
25
|
+
@flow_event_name ||= "business_flow.flow.#{flow_name}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module BusinessFlow
|
2
|
+
# Fire ActiveSupport events for every step that's run and on errors
|
3
|
+
class InstrumentedStepExecutor < InstrumentedExecutor
|
4
|
+
protected
|
5
|
+
|
6
|
+
def execute_step(step)
|
7
|
+
i_name = event_name(step)
|
8
|
+
i_payload = { flow: flow, step: step }
|
9
|
+
ActiveSupport::Notifications.instrument(i_name, i_payload) do |payload|
|
10
|
+
payload[:step_result] = super
|
11
|
+
end
|
12
|
+
notify_errors(i_name, i_payload)
|
13
|
+
end
|
14
|
+
|
15
|
+
def step_event_name(step)
|
16
|
+
"#{flow_name}.#{step.to_s.underscore}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def event_name(step)
|
20
|
+
"business_flow.step.#{step_event_name(step)}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/business_flow/step.rb
CHANGED
@@ -35,19 +35,17 @@ module BusinessFlow
|
|
35
35
|
# Represents the result of a step, and allows setting response values on
|
36
36
|
# an object, and merging error data into the same object.
|
37
37
|
class Result
|
38
|
+
# :reek:ManualDispatch Checking respond_to? is signficantly faster than
|
39
|
+
# eating the NoMethodError when grabbing our error object.
|
38
40
|
def initialize(result, output_map, output_callable)
|
39
41
|
@result = result
|
40
42
|
@output_map = output_map
|
41
43
|
@output_callable = output_callable
|
42
|
-
@result_errors =
|
43
|
-
result.errors
|
44
|
-
rescue NoMethodError
|
45
|
-
nil
|
46
|
-
end
|
44
|
+
@result_errors = result.respond_to?(:errors) ? result.errors : nil
|
47
45
|
end
|
48
46
|
|
49
47
|
def merge_into(object)
|
50
|
-
merge_errors_into(object
|
48
|
+
merge_errors_into(object)
|
51
49
|
merge_outputs_into(object)
|
52
50
|
end
|
53
51
|
|
@@ -74,11 +72,11 @@ module BusinessFlow
|
|
74
72
|
|
75
73
|
private
|
76
74
|
|
77
|
-
def merge_errors_into(
|
75
|
+
def merge_errors_into(object)
|
78
76
|
return if @result_errors.blank?
|
79
77
|
@result_errors.each do |attribute, message|
|
80
78
|
attribute = "#{@result.class.name.underscore}.#{attribute}"
|
81
|
-
(errors[attribute] << message).uniq!
|
79
|
+
(object.errors[attribute] << message).uniq!
|
82
80
|
end
|
83
81
|
throw :halt_step
|
84
82
|
end
|
@@ -111,13 +109,56 @@ module BusinessFlow
|
|
111
109
|
end
|
112
110
|
end
|
113
111
|
|
112
|
+
# Handle some logic around conditions
|
113
|
+
class ConditionList
|
114
|
+
def initialize(if_stmts, unless_stmts)
|
115
|
+
@conditions = Array.wrap(if_stmts).map(&Callable.method(:new)) +
|
116
|
+
Array.wrap(unless_stmts).map(&method(:to_unless))
|
117
|
+
end
|
118
|
+
|
119
|
+
def call(instance, inputs)
|
120
|
+
@conditions.all? { |cond| cond.call(instance, inputs) }
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def to_unless(cond)
|
126
|
+
if_stmt = Callable.new(cond)
|
127
|
+
unless_stmt = proc do |instance, input|
|
128
|
+
!if_stmt.call(instance, input)
|
129
|
+
end
|
130
|
+
Callable.new(unless_stmt)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Responsible for creating objects based on our input options
|
135
|
+
Options = Struct.new(:opts) do
|
136
|
+
def input_object
|
137
|
+
Inputs.new(opts[:inputs] || {})
|
138
|
+
end
|
139
|
+
|
140
|
+
def result_factory
|
141
|
+
ResultFactory.new(opts[:outputs] || {},
|
142
|
+
opts[:output] || ->(result) { result })
|
143
|
+
end
|
144
|
+
|
145
|
+
def condition
|
146
|
+
opts.fetch(:condition) do
|
147
|
+
if_stmts = opts[:if]
|
148
|
+
unless_stmts = opts[:unless]
|
149
|
+
if if_stmts.present? || unless_stmts.present?
|
150
|
+
ConditionList.new(if_stmts, unless_stmts)
|
151
|
+
end
|
152
|
+
end || proc { true }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
114
156
|
def initialize(callable, opts)
|
115
157
|
@callable = callable
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
@
|
120
|
-
@condition = opts[:condition] || proc { true }
|
158
|
+
opts = Options.new(opts)
|
159
|
+
@input_object = opts.input_object
|
160
|
+
@result_factory = opts.result_factory
|
161
|
+
@condition = opts.condition
|
121
162
|
end
|
122
163
|
|
123
164
|
def call(parameter_source)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module BusinessFlow
|
2
|
+
# Responsible for adding validations to flow objects
|
3
|
+
module Validations
|
4
|
+
# Additions to the DSL
|
5
|
+
module ClassMethods
|
6
|
+
# Declares that you expect to set this field during the course of
|
7
|
+
# processing, and that it should meet the given ActiveModel
|
8
|
+
# validations.
|
9
|
+
def expects(field, options = {})
|
10
|
+
validates field, options.merge(on: field)
|
11
|
+
::BusinessFlow::DSL::Field.new(field).add_to(self)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.included(klass)
|
16
|
+
klass.include(ActiveModel::Validations)
|
17
|
+
klass.class_eval do
|
18
|
+
class << self
|
19
|
+
alias_method :invariant, :validates
|
20
|
+
end
|
21
|
+
end
|
22
|
+
klass.extend ClassMethods
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/business_flow.rb
CHANGED
@@ -1,14 +1,17 @@
|
|
1
1
|
require 'active_model'
|
2
2
|
require 'active_support/core_ext'
|
3
3
|
require 'business_flow/version'
|
4
|
-
require 'business_flow/not_nil_validator'
|
5
4
|
require 'business_flow/flow_failed_exception'
|
6
5
|
require 'business_flow/callable'
|
7
6
|
require 'business_flow/step'
|
8
7
|
require 'business_flow/default_step_executor'
|
8
|
+
require 'business_flow/instrumented_executor'
|
9
|
+
require 'business_flow/instrumented_step_executor'
|
9
10
|
require 'business_flow/dsl'
|
11
|
+
require 'business_flow/validations'
|
10
12
|
require 'business_flow/base'
|
11
13
|
require 'business_flow/cacheable'
|
14
|
+
require 'business_flow/instrument'
|
12
15
|
|
13
16
|
# Makes the magic happen.
|
14
17
|
module BusinessFlow
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: business_flow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alex Scarborough
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-04-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -175,8 +175,11 @@ files:
|
|
175
175
|
- lib/business_flow/default_step_executor.rb
|
176
176
|
- lib/business_flow/dsl.rb
|
177
177
|
- lib/business_flow/flow_failed_exception.rb
|
178
|
-
- lib/business_flow/
|
178
|
+
- lib/business_flow/instrument.rb
|
179
|
+
- lib/business_flow/instrumented_executor.rb
|
180
|
+
- lib/business_flow/instrumented_step_executor.rb
|
179
181
|
- lib/business_flow/step.rb
|
182
|
+
- lib/business_flow/validations.rb
|
180
183
|
- lib/business_flow/version.rb
|
181
184
|
homepage: https://teak.io
|
182
185
|
licenses:
|
@@ -1,14 +0,0 @@
|
|
1
|
-
module BusinessFlow
|
2
|
-
# Validate that a given value is not nil, but allow blank/empty values.
|
3
|
-
class NotNilValidator < ActiveModel::Validator
|
4
|
-
# :reek:NilCheck :reek:UtilityFunction
|
5
|
-
# Dear reek -- I didn't decided the ActiveModel Validator API, I just
|
6
|
-
# have to live with it.
|
7
|
-
def validate(record)
|
8
|
-
record.class.needs.each do |attribute|
|
9
|
-
value = record.read_attribute_for_validation(attribute)
|
10
|
-
record.errors[attribute] << 'must not be nil' if value.nil?
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|