flows 0.4.0 → 0.5.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.mdlrc +1 -1
  3. data/.reek.yml +12 -0
  4. data/CHANGELOG.md +16 -0
  5. data/Gemfile.lock +1 -1
  6. data/README.md +12 -2
  7. data/Rakefile +1 -1
  8. data/bin/all_the_errors +8 -0
  9. data/bin/errors +12 -0
  10. data/bin/errors_cli/flow_error_demo.rb +22 -0
  11. data/docs/README.md +1 -1
  12. data/lib/flows/contract/case_eq.rb +3 -1
  13. data/lib/flows/flow/errors.rb +29 -0
  14. data/lib/flows/flow/node.rb +1 -0
  15. data/lib/flows/flow/router/custom.rb +5 -0
  16. data/lib/flows/flow/router/simple.rb +5 -0
  17. data/lib/flows/flow/router.rb +4 -0
  18. data/lib/flows/flow.rb +21 -0
  19. data/lib/flows/plugin/dependency_injector.rb +5 -5
  20. data/lib/flows/plugin/output_contract/dsl.rb +15 -3
  21. data/lib/flows/plugin/output_contract/wrapper.rb +14 -12
  22. data/lib/flows/plugin/output_contract.rb +1 -0
  23. data/lib/flows/plugin/profiler/injector.rb +35 -0
  24. data/lib/flows/plugin/profiler/report/events.rb +43 -0
  25. data/lib/flows/plugin/profiler/report/flat/method_report.rb +81 -0
  26. data/lib/flows/plugin/profiler/report/flat.rb +41 -0
  27. data/lib/flows/plugin/profiler/report/raw.rb +15 -0
  28. data/lib/flows/plugin/profiler/report/tree/calculated_node.rb +116 -0
  29. data/lib/flows/plugin/profiler/report/tree/node.rb +35 -0
  30. data/lib/flows/plugin/profiler/report/tree.rb +98 -0
  31. data/lib/flows/plugin/profiler/report.rb +48 -0
  32. data/lib/flows/plugin/profiler/wrapper.rb +53 -0
  33. data/lib/flows/plugin/profiler.rb +114 -0
  34. data/lib/flows/plugin.rb +1 -0
  35. data/lib/flows/railway/dsl.rb +3 -2
  36. data/lib/flows/result/do.rb +6 -8
  37. data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +38 -0
  38. data/lib/flows/shared_context_pipeline/dsl/tracks.rb +52 -0
  39. data/lib/flows/shared_context_pipeline/dsl.rb +5 -56
  40. data/lib/flows/shared_context_pipeline/mutation_step.rb +6 -8
  41. data/lib/flows/shared_context_pipeline/step.rb +6 -8
  42. data/lib/flows/shared_context_pipeline/track.rb +2 -15
  43. data/lib/flows/shared_context_pipeline/track_list.rb +11 -6
  44. data/lib/flows/shared_context_pipeline/wrap.rb +64 -0
  45. data/lib/flows/shared_context_pipeline.rb +109 -26
  46. data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +40 -51
  47. data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +39 -52
  48. data/lib/flows/util/inheritable_singleton_vars.rb +22 -15
  49. data/lib/flows/util/prepend_to_class.rb +43 -9
  50. data/lib/flows/version.rb +1 -1
  51. metadata +18 -2
@@ -19,11 +19,9 @@ module Flows
19
19
 
20
20
  Step.const_set(
21
21
  :NODE_PREPROCESSOR,
22
- lambda do |_input, context, meta|
23
- context[:last_step] = meta[:name]
24
-
22
+ lambda do |_input, context, node_meta|
25
23
  context[:class].before_each_callbacks.each do |callback|
26
- callback.call(context[:class], meta[:name], context[:data])
24
+ callback.call(context[:class], node_meta[:name], context[:data], context[:meta])
27
25
  end
28
26
 
29
27
  [EMPTY_ARRAY, context[:data]]
@@ -32,14 +30,14 @@ module Flows
32
30
 
33
31
  Step.const_set(
34
32
  :NODE_POSTPROCESSOR,
35
- lambda do |output, context, meta|
36
- context[:data].merge!(output.instance_variable_get(:@data))
33
+ lambda do |result, context, node_meta|
34
+ context[:data].merge!(result.instance_variable_get(:@data))
37
35
 
38
36
  context[:class].after_each_callbacks.each do |callback|
39
- callback.call(context[:class], meta[:name], context[:data], output)
37
+ callback.call(context[:class], node_meta[:name], result, context[:data], context[:meta])
40
38
  end
41
39
 
42
- output
40
+ result
43
41
  end
44
42
  )
45
43
  end
@@ -16,22 +16,9 @@ module Flows
16
16
  @step_list = @step_list.map(&:dup)
17
17
  end
18
18
 
19
- def add_step(name:, lambda: nil, router_def:)
20
- step = Step.new(name: name, lambda: lambda, router_def: router_def)
21
-
22
- last_step = @step_list.last
23
- last_step.next_step = name if last_step
24
-
25
- @step_list << step
26
-
27
- self
28
- end
29
-
30
- def add_mutation_step(name:, lambda: nil, router_def:)
31
- step = MutationStep.new(name: name, lambda: lambda, router_def: router_def)
32
-
19
+ def add_step(step)
33
20
  last_step = @step_list.last
34
- last_step.next_step = name if last_step
21
+ last_step.next_step = step.name if last_step
35
22
 
36
23
  @step_list << step
37
24
 
@@ -18,12 +18,8 @@ module Flows
18
18
  @current_track = track_name
19
19
  end
20
20
 
21
- def add_step(name:, lambda:, router_def:)
22
- @tracks[@current_track].add_step(name: name, lambda: lambda, router_def: router_def)
23
- end
24
-
25
- def add_mutation_step(name:, lambda:, router_def:)
26
- @tracks[@current_track].add_mutation_step(name: name, lambda: lambda, router_def: router_def)
21
+ def add_step(step)
22
+ @tracks[@current_track].add_step(step)
27
23
  end
28
24
 
29
25
  def first_step_name
@@ -41,6 +37,15 @@ module Flows
41
37
  )
42
38
  end
43
39
  end
40
+
41
+ def to_flow(method_source)
42
+ raise NoStepsError, method_source if main_track_empty?
43
+
44
+ Flows::Flow.new(
45
+ start_node: first_step_name,
46
+ node_map: to_node_map(method_source)
47
+ )
48
+ end
44
49
  end
45
50
  end
46
51
  end
@@ -0,0 +1,64 @@
1
+ module Flows
2
+ class SharedContextPipeline
3
+ # @api private
4
+ class Wrap
5
+ attr_reader :router_def
6
+
7
+ # :reek:Attribute:
8
+ attr_accessor :next_step
9
+
10
+ EMPTY_HASH = {}.freeze
11
+
12
+ NODE_PREPROCESSOR = lambda do |_input, context, _node_meta|
13
+ [[context], EMPTY_HASH]
14
+ end
15
+
16
+ NODE_POSTPROCESSOR = lambda do |result, context, _node_meta|
17
+ context[:data].merge!(result.instance_variable_get(:@data))
18
+
19
+ result
20
+ end
21
+
22
+ def initialize(method_name:, router_def:, &tracks_definitions)
23
+ @method_name = method_name
24
+ @router_def = router_def
25
+
26
+ singleton_class.extend DSL::Tracks
27
+ singleton_class.extend Result::Helpers
28
+
29
+ singleton_class.instance_exec(&tracks_definitions)
30
+ end
31
+
32
+ def name
33
+ singleton_class.tracks.first_step_name
34
+ end
35
+
36
+ def to_node(method_source)
37
+ Flows::Flow::Node.new(
38
+ body: make_body(method_source),
39
+ router: router_def.to_router(next_step),
40
+ meta: { wrap_name: @method_name },
41
+ preprocessor: NODE_PREPROCESSOR,
42
+ postprocessor: NODE_POSTPROCESSOR
43
+ )
44
+ end
45
+
46
+ private
47
+
48
+ def make_flow(method_source)
49
+ singleton_class.tracks.to_flow(method_source)
50
+ end
51
+
52
+ def make_body(method_source)
53
+ flow = make_flow(method_source)
54
+ wrapper = method_source.method(@method_name)
55
+
56
+ lambda do |context|
57
+ wrapper.call(context[:data], context[:meta]) do
58
+ flow.call(nil, context: context)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -4,6 +4,7 @@ require_relative 'shared_context_pipeline/step'
4
4
  require_relative 'shared_context_pipeline/mutation_step'
5
5
  require_relative 'shared_context_pipeline/track'
6
6
  require_relative 'shared_context_pipeline/track_list'
7
+ require_relative 'shared_context_pipeline/wrap'
7
8
  require_relative 'shared_context_pipeline/dsl'
8
9
 
9
10
  module Flows
@@ -140,35 +141,124 @@ module Flows
140
141
  # # steps implementations here
141
142
  # end
142
143
  #
143
- # ## Callbacks
144
+ # ## Wrappers
145
+ #
146
+ # Sometimes you have to execute some steps inside SQL-transaction or something like this.
147
+ # Most frameworks allow to do it in the following approach:
148
+ #
149
+ # SQLDataBase.transaction do
150
+ # # your steps are executed here
151
+ # # special error must be executed to cancel the transaction
152
+ # end
153
+ #
154
+ # It's impossible to do with just step or track DSL. That's why `wrap` DSL method has been added.
155
+ # Let's review it on example:
156
+ #
157
+ # class MySCP < Flows::SharedContextPipeline
158
+ # step :some_preparations
159
+ # wrap :in_transaction do
160
+ # step :action_a
161
+ # step :action_b
162
+ # end
163
+ #
164
+ # def in_transaction(ctx, meta, &block)
165
+ # result = nil
166
+ #
167
+ # ActiveRecord::Base.transaction do
168
+ # result = block.call
169
+ #
170
+ # raise ActiveRecord::Rollback if result.err?
171
+ # end
172
+ #
173
+ # result
174
+ # end
175
+ #
176
+ # # step implementations here
177
+ # end
178
+ #
179
+ # `wrap` DSL method receives name and block. Inside block you can define steps and tracks.
180
+ #
181
+ # `wrap` makes an isolated track and step structure.
182
+ # You cannot route between wrapped and unwrapped steps and tracks.
183
+ # One exception - you can route to the first wrapped step.
184
+ #
185
+ # The same wrapper with the same name can be used multiple times in the same operation:
186
+ #
187
+ # class MySCP < Flows::SharedContextPipeline
188
+ # step :some_preparations
189
+ # wrap :in_transaction do
190
+ # step :action_a
191
+ # step :action_b
192
+ # end
193
+ # step :some_calculations
194
+ # wrap :in_transaction do
195
+ # step :action_c
196
+ # step :action_d
197
+ # end
198
+ #
199
+ # # ...
200
+ # end
201
+ #
202
+ # Unlike step implementations wrapper implementation has access to a shared meta (can be useful for plugins).
203
+ #
204
+ # You may think about steps and tracks inside wrapper as a nested pipeline.
205
+ # Wrapper implementation receives mutable data context, metadata and block.
206
+ # Block execution (`block.call`) returns a result object of the executed "nested pipeline".
207
+ #
208
+ # When you route to `:end` inside wrapper - you're leaving wrapper, **not** the whole pipeline.
209
+ #
210
+ # From the execution perspective wrapper is a single step. The step name is the first wrapped step name.
211
+ #
212
+ # `wrap` itself also can have overriden routes table:
213
+ #
214
+ # wrap :in_transaction, routes(match_ok => :next, match_err => :end) do
215
+ # # steps...
216
+ # end
217
+ #
218
+ # Like a step, wrapper implementation must return {Flows::Result}.
219
+ # Result is processed with the same approach as for normal step.
220
+ # **Do not modify result returned from block - build a new one if needed.
221
+ # Otherwise mutation steps can be broken.**
222
+ #
223
+ # ## Callbacks and metadata
144
224
  #
145
225
  # You may want to have some logic to execute before all steps, or after all, or before each, or after each.
146
226
  # For example to inject generalized execution process logging.
147
227
  # To achieve this you can use callbacks:
148
228
  #
149
229
  # class MySCP < Flows::SharedContextPipeline
150
- # before_all do |klass, context|
151
- # # you can modify execution context here
230
+ # before_all do |klass, data, meta|
231
+ # # you can modify execution data context and metadata here
152
232
  # # return value will be ignored
153
233
  # end
154
234
  #
155
- # after_all do |klass, pipeline_result|
156
- # # you must provide final result object for pipeline here
235
+ # after_all do |klass, pipeline_result, data, meta|
236
+ # # you can modify execution data context and metadata here
237
+ # # you must return a final result object here
157
238
  # # if no modifications needed - just return provided pipeline_result
158
239
  # end
159
240
  #
160
- # before_each do |klass, step_name, context|
161
- # # you can modify context here
241
+ # before_each do |klass, step_name, data, meta|
242
+ # # you can modify execution data context and metadata here
162
243
  # # return value will be ignored
163
244
  # end
164
245
  #
165
- # after_each do |klass, step_name, context, step_result|
166
- # # you can modify context here
167
- # # you must not modify step_result
168
- # # context already has data from step_result at the moment of execution
246
+ # after_each do |klass, step_name, step_result, data, meta|
247
+ # # you can modify execution data context and metadata here
169
248
  # # return value will be ignored
249
+ # #
250
+ # # callback executed after context is updated with result data
251
+ # # (in the case of normal steps, mutation steps update context directly)
252
+ # #
253
+ # # DO NOT MODIFY RESULT OBJECT HERE - IT CAN BROKE MUTATION STEPS
170
254
  # end
171
255
  # end
256
+ #
257
+ # Metadata - is a Hash which is shared between step executions.
258
+ # This hash becomes metadata of a final {Flows::Result}.
259
+ #
260
+ # Metadata is designed to store non-business data such as execution times,
261
+ # some library specific data, and so on.
172
262
  class SharedContextPipeline
173
263
  extend ::Flows::Plugin::ImplicitInit
174
264
 
@@ -178,38 +268,31 @@ module Flows
178
268
  extend DSL
179
269
 
180
270
  def initialize
181
- klass = self.class
182
- tracks = klass.tracks
183
-
184
- raise NoStepsError, klass if tracks.main_track_empty?
185
-
186
- @__flow = Flows::Flow.new(
187
- start_node: tracks.first_step_name,
188
- node_map: tracks.to_node_map(self)
189
- )
271
+ @__flow = self.class.tracks.to_flow(self)
190
272
  end
191
273
 
192
274
  # Executes pipeline with provided keyword arguments, returns Result Object.
193
275
  #
194
276
  # @return [Flows::Result]
195
- def call(**kwargs) # rubocop:disable Metrics/MethodLength
277
+ def call(**data) # rubocop:disable Metrics/MethodLength
196
278
  klass = self.class
197
- context = { data: kwargs, class: klass }
279
+ meta = {}
280
+ context = { data: data, meta: meta, class: klass }
198
281
 
199
282
  klass.before_all_callbacks.each do |callback|
200
- callback.call(klass, context[:data])
283
+ callback.call(klass, data, meta)
201
284
  end
202
285
 
203
286
  flow_result = @__flow.call(nil, context: context)
204
287
 
205
288
  final_result = flow_result.class.new(
206
- context[:data],
289
+ data,
207
290
  status: flow_result.status,
208
- meta: { last_step: context[:last_step] }
291
+ meta: meta
209
292
  )
210
293
 
211
294
  klass.after_all_callbacks.reduce(final_result) do |result, callback|
212
- callback.call(klass, result)
295
+ callback.call(klass, result, data, meta)
213
296
  end
214
297
  end
215
298
  end
@@ -23,85 +23,74 @@ module Flows
23
23
  VAR_LIST_VAR_NAME = :@inheritable_vars_with_dup
24
24
 
25
25
  # @api private
26
- module InheritanceCallback
27
- def inherited(child_class)
28
- DupStrategy.migrate(self, child_class)
26
+ module Migrator
27
+ def self.call(from, to)
28
+ parent_var_list = from.instance_variable_get(VAR_LIST_VAR_NAME)
29
+ child_var_list = to.instance_variable_get(VAR_LIST_VAR_NAME) || []
29
30
 
30
- super
31
- end
31
+ to.instance_variable_set(VAR_LIST_VAR_NAME, child_var_list + parent_var_list)
32
32
 
33
- def included(child_mod)
34
- DupStrategy.migrate(self, child_mod)
33
+ parent_var_list.each do |name|
34
+ to.instance_variable_set(name, from.instance_variable_get(name).dup)
35
+ end
36
+ end
37
+ end
35
38
 
36
- child_mod.singleton_class.prepend(InheritanceCallback)
39
+ # @api private
40
+ module Injector
41
+ def included(mod)
42
+ Migrator.call(self, mod)
43
+ mod.singleton_class.prepend Injector
37
44
 
38
45
  super
39
46
  end
40
47
 
41
- def extended(child_mod)
42
- DupStrategy.migrate(self, child_mod)
48
+ def extended(mod)
49
+ Migrator.call(self, mod)
50
+ mod.singleton_class.prepend Injector
43
51
 
44
- child_mod.singleton_class.prepend(InheritanceCallback)
52
+ super
53
+ end
54
+
55
+ def inherited(mod)
56
+ Migrator.call(self, mod)
57
+ mod.singleton_class.prepend Injector
45
58
 
46
59
  super
47
60
  end
48
61
  end
49
62
 
50
63
  class << self
51
- # Applies behaviour and defaults for singleton variables.
52
- #
53
- # @note Variable names should look like `:@var` or `'@var'`.
54
- #
55
- # @param klass [Class] target class.
56
- # @param attrs_with_default [Hash<Symbol, String => Object>] keys are variable names,
57
- # values are default values.
64
+ # Generates a module which applies behaviour and defaults for singleton variables.
58
65
  #
59
66
  # @example
60
67
  # class MyClass
61
- # Flows::Util::InheritableSingletonVars::DupStrategy.call(
62
- # self,
68
+ # SingletonVarsSetup = Flows::Util::InheritableSingletonVars::DupStrategy.make_module(
63
69
  # :@my_list => []
64
70
  # )
71
+ #
72
+ # include SingletonVarsSetup
65
73
  # end
66
- def call(klass, attrs_with_default = {})
67
- init_variables_with_default_values(klass, attrs_with_default)
68
-
69
- var_names = attrs_with_default.keys.map(&:to_sym)
70
- add_var_list(klass, var_names)
71
-
72
- inject_inheritance_hook(klass)
73
- end
74
-
75
- # Moves variables between modules
76
74
  #
77
- # @api private
78
- def migrate(from_mod, to_mod)
79
- var_list = from_mod.instance_variable_get(VAR_LIST_VAR_NAME)
80
- to_mod.instance_variable_set(VAR_LIST_VAR_NAME, var_list.dup)
81
-
82
- var_list.each do |name|
83
- to_mod.instance_variable_set(name, from_mod.instance_variable_get(name).dup)
75
+ # @note Variable names should look like `:@var` or `'@var'`.
76
+ #
77
+ # @param vars_with_default [Hash<Symbol, String => Object>] keys are variable names,
78
+ # values are default values.
79
+ def make_module(vars_with_default = {})
80
+ Module.new.tap do |mod|
81
+ mod.instance_variable_set(VAR_LIST_VAR_NAME, vars_with_default.keys.map(&:to_sym))
82
+ init_vars(mod, vars_with_default)
83
+ mod.extend Injector
84
84
  end
85
85
  end
86
86
 
87
87
  private
88
88
 
89
- def init_variables_with_default_values(klass, attrs_with_default)
90
- attrs_with_default.each do |name, default_value|
91
- klass.instance_variable_set(name, default_value)
89
+ def init_vars(mod, vars_with_default)
90
+ vars_with_default.each do |name, value|
91
+ mod.instance_variable_set(name, value)
92
92
  end
93
93
  end
94
-
95
- def add_var_list(klass, var_names)
96
- watch_list = klass.instance_variable_get(VAR_LIST_VAR_NAME) || []
97
- watch_list.concat(var_names)
98
- klass.instance_variable_set(VAR_LIST_VAR_NAME, watch_list)
99
- end
100
-
101
- def inject_inheritance_hook(klass)
102
- singleton = klass.singleton_class
103
- singleton.prepend(InheritanceCallback) unless singleton.is_a?(InheritanceCallback)
104
- end
105
94
  end
106
95
  end
107
96
  end