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.
- checksums.yaml +4 -4
- data/.mdlrc +1 -1
- data/.reek.yml +12 -0
- data/CHANGELOG.md +16 -0
- data/Gemfile.lock +1 -1
- data/README.md +12 -2
- data/Rakefile +1 -1
- data/bin/all_the_errors +8 -0
- data/bin/errors +12 -0
- data/bin/errors_cli/flow_error_demo.rb +22 -0
- data/docs/README.md +1 -1
- data/lib/flows/contract/case_eq.rb +3 -1
- data/lib/flows/flow/errors.rb +29 -0
- data/lib/flows/flow/node.rb +1 -0
- data/lib/flows/flow/router/custom.rb +5 -0
- data/lib/flows/flow/router/simple.rb +5 -0
- data/lib/flows/flow/router.rb +4 -0
- data/lib/flows/flow.rb +21 -0
- data/lib/flows/plugin/dependency_injector.rb +5 -5
- data/lib/flows/plugin/output_contract/dsl.rb +15 -3
- data/lib/flows/plugin/output_contract/wrapper.rb +14 -12
- data/lib/flows/plugin/output_contract.rb +1 -0
- data/lib/flows/plugin/profiler/injector.rb +35 -0
- data/lib/flows/plugin/profiler/report/events.rb +43 -0
- data/lib/flows/plugin/profiler/report/flat/method_report.rb +81 -0
- data/lib/flows/plugin/profiler/report/flat.rb +41 -0
- data/lib/flows/plugin/profiler/report/raw.rb +15 -0
- data/lib/flows/plugin/profiler/report/tree/calculated_node.rb +116 -0
- data/lib/flows/plugin/profiler/report/tree/node.rb +35 -0
- data/lib/flows/plugin/profiler/report/tree.rb +98 -0
- data/lib/flows/plugin/profiler/report.rb +48 -0
- data/lib/flows/plugin/profiler/wrapper.rb +53 -0
- data/lib/flows/plugin/profiler.rb +114 -0
- data/lib/flows/plugin.rb +1 -0
- data/lib/flows/railway/dsl.rb +3 -2
- data/lib/flows/result/do.rb +6 -8
- data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +38 -0
- data/lib/flows/shared_context_pipeline/dsl/tracks.rb +52 -0
- data/lib/flows/shared_context_pipeline/dsl.rb +5 -56
- data/lib/flows/shared_context_pipeline/mutation_step.rb +6 -8
- data/lib/flows/shared_context_pipeline/step.rb +6 -8
- data/lib/flows/shared_context_pipeline/track.rb +2 -15
- data/lib/flows/shared_context_pipeline/track_list.rb +11 -6
- data/lib/flows/shared_context_pipeline/wrap.rb +64 -0
- data/lib/flows/shared_context_pipeline.rb +109 -26
- data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +40 -51
- data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +39 -52
- data/lib/flows/util/inheritable_singleton_vars.rb +22 -15
- data/lib/flows/util/prepend_to_class.rb +43 -9
- data/lib/flows/version.rb +1 -1
- 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,
|
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],
|
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 |
|
36
|
-
context[:data].merge!(
|
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],
|
37
|
+
callback.call(context[:class], node_meta[:name], result, context[:data], context[:meta])
|
40
38
|
end
|
41
39
|
|
42
|
-
|
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(
|
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(
|
22
|
-
@tracks[@current_track].add_step(
|
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
|
-
# ##
|
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,
|
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
|
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,
|
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,
|
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
|
-
|
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(**
|
277
|
+
def call(**data) # rubocop:disable Metrics/MethodLength
|
196
278
|
klass = self.class
|
197
|
-
|
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,
|
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
|
-
|
289
|
+
data,
|
207
290
|
status: flow_result.status,
|
208
|
-
meta:
|
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
|
27
|
-
def
|
28
|
-
|
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
|
-
|
31
|
-
end
|
31
|
+
to.instance_variable_set(VAR_LIST_VAR_NAME, child_var_list + parent_var_list)
|
32
32
|
|
33
|
-
|
34
|
-
|
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
|
-
|
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(
|
42
|
-
|
48
|
+
def extended(mod)
|
49
|
+
Migrator.call(self, mod)
|
50
|
+
mod.singleton_class.prepend Injector
|
43
51
|
|
44
|
-
|
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
|
-
#
|
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.
|
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
|
-
# @
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
90
|
-
|
91
|
-
|
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
|