business_flow 0.7.0 → 0.8.0

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