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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: aa83401ab6f440779738905d43a9b7114ed03e68
4
- data.tar.gz: 5af8dbff7edf19b183a8201f418ef3555aadd54a
3
+ metadata.gz: 430484fefee99d40a5f0043d018409179459cf87
4
+ data.tar.gz: 28aa9f6aabb03d731824498e96c00d7bb23aa224
5
5
  SHA512:
6
- metadata.gz: 9d44e6d0dd4d71e140fe4f4fff24b1243295355cd71c4e128585a08c0abab3cb93f5da9ea28db39cdd25a7088fa8e03c53ca4fe35623fe63b2c7dbd45df71cd2
7
- data.tar.gz: f16efd204e218ffd86a0850cefa452b338171164d7fb1ded0bb157745637459e9c49a843acfb4ee5366a8bddc7f370dab3252df8fe7d7abd7762702d8c35da7f
6
+ metadata.gz: 2bb43af5b2ff636c7e1cd78cf6b6b0e5c4211969c5edac41d999a75f6e3f768921053dd6c4c4acd8119de9f61a290636095fc784fbc3af19a188ccc1577d2d9d
7
+ data.tar.gz: 775da5c4e2a71d925e7963d34ec97e78773fd69bf057e85f476b578788740ea7bf7f25832bd0ff9b89a9f1336ee60f114b208e9ef1071bf620ef99717df87cb0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- business_flow (0.7.0)
4
+ business_flow (0.8.0)
5
5
  activemodel (>= 3.0)
6
6
  activesupport (>= 3.0)
7
7
 
@@ -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.prepend(InstanceMethods)
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
- @cache_ttl = ttl
45
+ cache_options.ttl = ttl
31
46
  else
32
- @cache_ttl
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, self)
53
+ @cache_key = Callable.new(key)
39
54
  else
40
- @cache_key ||= Callable.new(:parameter_object, self)
55
+ @cache_key ||= Callable.new(:parameter_object)
41
56
  end
42
57
  end
43
58
 
44
- def call(*args)
45
- flow = new(*args)
46
- PrivateHelpers.execute(self, flow)
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
- end
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, metaclass)
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
- cached_proc.call(instance, inputs)
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
- # :reek:ManualDispatch Look reek I don't know what you want me to do.
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
- @cached_proc = proc_callable
26
- elsif @callable.respond_to?(:call)
27
- @cached_proc = call_callable
28
- elsif !@callable.is_a?(Symbol)
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 cached_proc
34
- @cached_proc ||=
35
- if @metaclass.method_defined?(@callable) ||
36
- @metaclass.private_method_defined?(@callable)
37
- proc { |instance, _| instance.send(@callable) }
38
- else
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 proc_callable
46
- proc do |instance, inputs|
47
- if @callable.arity.zero?
48
- instance.instance_exec(&@callable)
49
- else
50
- instance.instance_exec(inputs, &@callable)
51
- end
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 call_callable
56
- proc do |instance, inputs|
57
- case @callable.method(:call).arity
58
- when 1, -1
70
+ def single_inputs_callable
71
+ instance_eval %{
72
+ def call(_instance, inputs)
59
73
  @callable.call(inputs)
60
- when 0
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
- else
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
- end
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
- @metaclass.parents.each do |parent|
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
- return if @flow.errors.present?
13
- ActiveSupport::Notifications.instrument(flow_event_name, flow: @flow) do
14
- @step_queue.each do |step|
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
- def process_step(step)
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
- ActiveSupport::Notifications.instrument(
33
- event_name(step), flow: @flow, step: step
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
@@ -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
- return @needs if fields.blank?
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
- PrivateHelpers.create_parameter_field(self, field, internal_name)
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
- return @provides if fields.blank?
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
- callable = Callable.new(klass, self)
51
- step = Step.new(callable, opts)
52
- PrivateHelpers.create_memoized_field(self, field, step)
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
- callable = Callable.new(klass, self)
58
- opts = opts.merge(
59
- condition: PrivateHelpers.create_conditional_callable(self, opts)
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
- new(parameter_object).tap(&:call)
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
- ret = call(*args)
73
- raise FlowFailedException, ret if ret.errors.any?
74
- ret
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
- klass.instance_eval do
88
- class << self
89
- # See above -- that this is an alias is considered public API.
90
- alias invariant validates
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
- # Keep our internal helpers in a different module to avoid polluting the
97
- # namespace of whoever includes us.
98
- module PrivateHelpers
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
- def call(instance, inputs)
108
- @conditions.all? { |cond| cond.call(instance, inputs) }
109
- end
113
+ def errors?
114
+ @errors.present?
115
+ end
110
116
 
111
- private
117
+ def valid?(_context = nil)
118
+ @errors.blank?
119
+ end
112
120
 
113
- def to_if(cond)
114
- Callable.new(cond, @klass)
115
- end
121
+ def invalid?(context = nil)
122
+ !valid?(context)
123
+ end
116
124
 
117
- def to_unless(cond)
118
- if_stmt = to_if(cond)
119
- unless_stmt = proc do |instance, input|
120
- !if_stmt.call(instance, input)
121
- end
122
- to_if(unless_stmt)
123
- end
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 self.create_parameter_field(klass, field, fallback = nil)
127
- klass.send(:define_method, field, &parameter_proc(field, fallback))
128
- klass.send(:private, field)
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
- def self.read_from_parameter_object(field)
132
- proc do
133
- if parameter_object.is_a?(Hash) && parameter_object.key?(field)
134
- parameter_object[field]
135
- else
136
- parameter_object.public_send(field)
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
- end
226
+ #{setter}
227
+ MEMOIZED
139
228
  end
140
229
 
141
- def self.parameter_proc(field, fallback)
142
- read = read_from_parameter_object(field)
143
- proc do
144
- begin
145
- instance_exec(&read)
146
- rescue NoMethodError
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 self.create_conditional_callable(klass, opts)
153
- return unless opts[:if] || opts[:unless]
154
- Callable.new(condition(klass, opts), klass)
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
- def self.condition(klass, opts)
158
- if_stmts = opts.fetch(:if, proc { true })
159
- unless_stmts = opts.fetch(:unless, proc { false })
160
- ConditionList.new(if_stmts, unless_stmts, klass)
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
- def self.create_field(klass, field)
164
- return unless field.is_a?(Symbol)
165
- define_getter(klass, field)
166
- setter_name = "#{field}=".to_sym
167
- define_setter(klass, setter_name, field)
168
- klass.send(:private, setter_name)
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 self.define_getter(klass, field)
172
- return if klass.method_defined?(field) ||
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
- def self.define_setter(klass, setter_name, field)
178
- return if klass.method_defined?(setter_name) ||
179
- klass.private_method_defined?(setter_name)
180
- klass.send(:define_method, setter_name,
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
- def self.safe_ivar_name(field)
185
- ivar_name = "@business_flow_dsl_#{field}"
186
- if ivar_name.end_with?('?')
187
- ivar_name.sub!(/\?$/, '_query')
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 self.setter_proc(ivar_name, field)
195
- proc do |new_value|
196
- instance_variable_set(ivar_name, new_value)
197
- throw :halt_step unless valid?(field)
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
- def self.create_memoized_field(klass, field, step)
203
- ivar_name = safe_ivar_name(field)
204
- setter_proc = self.setter_proc(ivar_name, field)
205
- klass.send(:define_method, field) do
206
- if instance_variable_defined?(ivar_name)
207
- instance_variable_get(ivar_name)
208
- else
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
@@ -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 = begin
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.errors)
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(errors)
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
- @input_object = Inputs.new(opts[:inputs] || {})
117
- outputs = opts[:outputs] || {}
118
- output = opts[:output] || ->(result) { result }
119
- @result_factory = ResultFactory.new(outputs, output)
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
@@ -1,3 +1,3 @@
1
1
  module BusinessFlow
2
- VERSION = '0.7.0'.freeze
2
+ VERSION = '0.8.0'.freeze
3
3
  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.7.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-03-30 00:00:00.000000000 Z
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/not_nil_validator.rb
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