flows 0.1.0 → 0.5.1
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/.github/workflows/test.yml +38 -0
- data/.gitignore +9 -1
- data/.mdlrc +1 -0
- data/.reek.yml +54 -0
- data/.rubocop.yml +44 -2
- data/.ruby-version +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +65 -0
- data/README.md +186 -256
- data/Rakefile +35 -1
- data/bin/.rubocop.yml +5 -0
- data/bin/all_the_errors +55 -0
- data/bin/benchmark +69 -78
- data/bin/benchmark_cli/compare.rb +118 -0
- data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
- data/bin/benchmark_cli/compare/base.rb +45 -0
- data/bin/benchmark_cli/compare/command.rb +47 -0
- data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
- data/bin/benchmark_cli/examples.rb +23 -0
- data/bin/benchmark_cli/examples/.rubocop.yml +19 -0
- data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
- data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
- data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
- data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
- data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
- data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
- data/bin/benchmark_cli/helpers.rb +12 -0
- data/bin/benchmark_cli/ruby.rb +15 -0
- data/bin/benchmark_cli/ruby/command.rb +38 -0
- data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
- data/bin/benchmark_cli/ruby/self_class.rb +69 -0
- data/bin/benchmark_cli/ruby/structs.rb +90 -0
- data/bin/console +1 -0
- data/bin/docserver +7 -0
- data/bin/errors +130 -0
- data/bin/errors_cli/contract_error_demo.rb +49 -0
- data/bin/errors_cli/di_error_demo.rb +38 -0
- data/bin/errors_cli/flow_error_demo.rb +22 -0
- data/bin/errors_cli/flows_router_error_demo.rb +15 -0
- data/bin/errors_cli/oc_error_demo.rb +40 -0
- data/bin/errors_cli/railway_error_demo.rb +10 -0
- data/bin/errors_cli/result_error_demo.rb +13 -0
- data/bin/errors_cli/scp_error_demo.rb +17 -0
- data/docs/.nojekyll +0 -0
- data/docs/README.md +13 -0
- data/docs/_sidebar.md +2 -0
- data/docs/index.html +30 -0
- data/flows.gemspec +27 -2
- data/forspell.dict +17 -0
- data/lefthook.yml +21 -0
- data/lib/flows.rb +13 -5
- data/lib/flows/contract.rb +402 -0
- data/lib/flows/contract/array.rb +55 -0
- data/lib/flows/contract/case_eq.rb +43 -0
- data/lib/flows/contract/compose.rb +77 -0
- data/lib/flows/contract/either.rb +53 -0
- data/lib/flows/contract/error.rb +25 -0
- data/lib/flows/contract/hash.rb +75 -0
- data/lib/flows/contract/hash_of.rb +70 -0
- data/lib/flows/contract/helpers.rb +22 -0
- data/lib/flows/contract/predicate.rb +34 -0
- data/lib/flows/contract/transformer.rb +50 -0
- data/lib/flows/contract/tuple.rb +70 -0
- data/lib/flows/flow.rb +96 -7
- data/lib/flows/flow/errors.rb +29 -0
- data/lib/flows/flow/node.rb +132 -0
- data/lib/flows/flow/router.rb +29 -0
- data/lib/flows/flow/router/custom.rb +59 -0
- data/lib/flows/flow/router/errors.rb +11 -0
- data/lib/flows/flow/router/simple.rb +25 -0
- data/lib/flows/plugin.rb +14 -0
- data/lib/flows/plugin/dependency_injector.rb +159 -0
- data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
- data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
- data/lib/flows/plugin/dependency_injector/dependency_list.rb +57 -0
- data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
- data/lib/flows/plugin/implicit_init.rb +45 -0
- data/lib/flows/plugin/output_contract.rb +85 -0
- data/lib/flows/plugin/output_contract/dsl.rb +48 -0
- data/lib/flows/plugin/output_contract/errors.rb +74 -0
- data/lib/flows/plugin/output_contract/wrapper.rb +55 -0
- data/lib/flows/plugin/profiler.rb +114 -0
- data/lib/flows/plugin/profiler/injector.rb +35 -0
- data/lib/flows/plugin/profiler/report.rb +48 -0
- data/lib/flows/plugin/profiler/report/events.rb +43 -0
- data/lib/flows/plugin/profiler/report/flat.rb +41 -0
- data/lib/flows/plugin/profiler/report/flat/method_report.rb +81 -0
- data/lib/flows/plugin/profiler/report/raw.rb +15 -0
- data/lib/flows/plugin/profiler/report/tree.rb +98 -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/wrapper.rb +53 -0
- data/lib/flows/railway.rb +154 -0
- data/lib/flows/railway/dsl.rb +18 -0
- data/lib/flows/railway/errors.rb +17 -0
- data/lib/flows/railway/step.rb +24 -0
- data/lib/flows/railway/step_list.rb +38 -0
- data/lib/flows/result.rb +189 -2
- data/lib/flows/result/do.rb +172 -0
- data/lib/flows/result/err.rb +12 -6
- data/lib/flows/result/errors.rb +29 -17
- data/lib/flows/result/helpers.rb +25 -3
- data/lib/flows/result/ok.rb +12 -6
- data/lib/flows/shared_context_pipeline.rb +299 -0
- data/lib/flows/shared_context_pipeline/dsl.rb +12 -0
- 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/errors.rb +17 -0
- data/lib/flows/shared_context_pipeline/mutation_step.rb +29 -0
- data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
- data/lib/flows/shared_context_pipeline/step.rb +44 -0
- data/lib/flows/shared_context_pipeline/track.rb +54 -0
- data/lib/flows/shared_context_pipeline/track_list.rb +51 -0
- data/lib/flows/shared_context_pipeline/wrap.rb +74 -0
- data/lib/flows/util.rb +17 -0
- data/lib/flows/util/inheritable_singleton_vars.rb +86 -0
- data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +98 -0
- data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +91 -0
- data/lib/flows/util/prepend_to_class.rb +179 -0
- data/lib/flows/version.rb +1 -1
- metadata +288 -20
- data/.travis.yml +0 -8
- data/Gemfile.lock +0 -119
- data/bin/demo +0 -66
- data/bin/examples.rb +0 -159
- data/bin/profile_10steps +0 -64
- data/bin/ruby_benchmarks +0 -26
- data/lib/flows/node.rb +0 -27
- data/lib/flows/operation.rb +0 -54
- data/lib/flows/operation/builder.rb +0 -130
- data/lib/flows/operation/builder/build_router.rb +0 -37
- data/lib/flows/operation/dsl.rb +0 -72
- data/lib/flows/operation/errors.rb +0 -75
- data/lib/flows/operation/executor.rb +0 -78
- data/lib/flows/result_router.rb +0 -14
- data/lib/flows/router.rb +0 -22
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module Flows
|
|
2
|
+
module Plugin
|
|
3
|
+
module Profiler
|
|
4
|
+
class Report
|
|
5
|
+
class Flat < Tree
|
|
6
|
+
# @api private
|
|
7
|
+
class MethodReport
|
|
8
|
+
attr_reader :root_node
|
|
9
|
+
attr_reader :calculated_nodes
|
|
10
|
+
|
|
11
|
+
def initialize(root_node, *calculated_nodes)
|
|
12
|
+
@root_node = root_node
|
|
13
|
+
@calculated_nodes = calculated_nodes
|
|
14
|
+
|
|
15
|
+
raise 'no single node provided' if calculated_nodes.empty?
|
|
16
|
+
raise 'calculated_nodes must be about the same subject' unless nodes_have_same_subject
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def subject
|
|
20
|
+
@subject ||= calculated_nodes.first.subject
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def count
|
|
24
|
+
@count ||= calculated_nodes.map(&:count).sum
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def total_self_ms
|
|
28
|
+
@total_self_ms ||= calculated_nodes.map(&:total_self_ms).sort.sum
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def total_self_percentage
|
|
32
|
+
@total_self_percentage ||= calculated_nodes
|
|
33
|
+
.map { |node| node.total_self_percentage(root_node) }
|
|
34
|
+
.sort
|
|
35
|
+
.sum
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def avg_self_ms
|
|
39
|
+
@avg_self_ms ||= total_self_ms / count
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def direct_subcalls
|
|
43
|
+
@direct_subcalls ||= calculated_nodes
|
|
44
|
+
.flat_map { |node| node.children.map(&:subject) }
|
|
45
|
+
.uniq
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_h
|
|
49
|
+
@to_h ||= {
|
|
50
|
+
subject: subject,
|
|
51
|
+
count: count,
|
|
52
|
+
total_self_ms: total_self_ms,
|
|
53
|
+
total_self_percentage: total_self_percentage,
|
|
54
|
+
avg_self_ms: avg_self_ms,
|
|
55
|
+
direct_subcalls: direct_subcalls
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_s
|
|
60
|
+
[
|
|
61
|
+
'',
|
|
62
|
+
"- #{subject} -",
|
|
63
|
+
"called: #{count} time(s)",
|
|
64
|
+
"total self execution time: #{total_self_ms.truncate(2)}ms",
|
|
65
|
+
"total self percentage: #{total_self_percentage.truncate(2)}%",
|
|
66
|
+
"average self execution time: #{avg_self_ms.truncate(2)}ms",
|
|
67
|
+
"direct subcalls: #{direct_subcalls.join(', ')}"
|
|
68
|
+
]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def nodes_have_same_subject
|
|
74
|
+
calculated_nodes.all? { |node| node.subject == subject }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require_relative 'tree/node'
|
|
2
|
+
require_relative 'tree/calculated_node'
|
|
3
|
+
|
|
4
|
+
module Flows
|
|
5
|
+
module Plugin
|
|
6
|
+
module Profiler
|
|
7
|
+
class Report
|
|
8
|
+
# Tree report. Merges similar calls, saves execution structure (who called whom).
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# Flows::Plugin::Profiler.profile(:tree) do
|
|
12
|
+
# # some code here
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# puts Flows::Plugin::Profiler.last_report
|
|
16
|
+
class Tree < Report
|
|
17
|
+
# Returns tree report as Ruby data structs.
|
|
18
|
+
#
|
|
19
|
+
# @return [Array<Hash>] tree report.
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# [
|
|
23
|
+
# {
|
|
24
|
+
# subject: 'MyClass#call',
|
|
25
|
+
# count: 2,
|
|
26
|
+
# total_ms: 100.0,
|
|
27
|
+
# total_self_ms: 80.0,
|
|
28
|
+
# total_self_percentage: 80.0,
|
|
29
|
+
# avg_ms: 50.0,
|
|
30
|
+
# avg_self_ms: 40.0,
|
|
31
|
+
# nested: [
|
|
32
|
+
# {
|
|
33
|
+
# subject: 'MyClass#another_method',
|
|
34
|
+
# count: 1,
|
|
35
|
+
# total_ms: 20.0,
|
|
36
|
+
# total_self_ms: 20.0,
|
|
37
|
+
# total_self_percentage: 20.0,
|
|
38
|
+
# avg_ms: 20.0,
|
|
39
|
+
# avg_self_ms: 20.0,
|
|
40
|
+
# nested: []
|
|
41
|
+
# }
|
|
42
|
+
# ]
|
|
43
|
+
# }
|
|
44
|
+
# ]
|
|
45
|
+
def to_a
|
|
46
|
+
root_calculated_node.children.map { |node| node.to_h(root_calculated_node) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def add(*)
|
|
50
|
+
forget_memoized_values
|
|
51
|
+
super
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_s
|
|
55
|
+
root_calculated_node.children.map { |node| node.to_s(root_calculated_node) }.join
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def forget_memoized_values
|
|
61
|
+
@root_node = nil
|
|
62
|
+
@root_calculated_node = nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def root_calculated_node
|
|
66
|
+
@root_calculated_node ||= CalculatedNode.new(root_node)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def root_node
|
|
70
|
+
@root_node ||= Node.new(subject: :ROOT).tap do |root_node|
|
|
71
|
+
events.each_with_object([root_node]) do |event, node_path|
|
|
72
|
+
current_node = node_path.last
|
|
73
|
+
|
|
74
|
+
case event
|
|
75
|
+
when StartEvent then process_start_event(node_path, current_node, event)
|
|
76
|
+
when FinishEvent then process_finish_event(node_path, current_node, event)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# :reek:UtilityFunction
|
|
83
|
+
def process_start_event(node_path, current_node, event)
|
|
84
|
+
node_path << current_node[event.subject]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# :reek:UtilityFunction :reek:FeatureEnvy
|
|
88
|
+
def process_finish_event(node_path, current_node, event)
|
|
89
|
+
raise 'Invalid profiling events detected' if event.subject != current_node.subject
|
|
90
|
+
|
|
91
|
+
current_node.register_execution(event.data)
|
|
92
|
+
node_path.pop
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
module Flows
|
|
2
|
+
module Plugin
|
|
3
|
+
module Profiler
|
|
4
|
+
class Report
|
|
5
|
+
class Tree < Report
|
|
6
|
+
# @api private
|
|
7
|
+
class CalculatedNode
|
|
8
|
+
MICROSECONDS_IN_MILLISECOND = 1000.0
|
|
9
|
+
|
|
10
|
+
attr_reader :children
|
|
11
|
+
|
|
12
|
+
def initialize(node)
|
|
13
|
+
@node = node
|
|
14
|
+
@children = node.children
|
|
15
|
+
.map { |child| self.class.new(child) }
|
|
16
|
+
.sort_by(&:total_ms)
|
|
17
|
+
.reverse
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def subject
|
|
21
|
+
@node.subject
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def count
|
|
25
|
+
@count ||= @node.executions.count
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def total_ms
|
|
29
|
+
@total_ms ||= @node.executions.sort.sum / MICROSECONDS_IN_MILLISECOND
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def avg_ms
|
|
33
|
+
@avg_ms ||= total_ms / count
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def children_ms
|
|
37
|
+
@children_ms ||= children.map(&:total_ms).sort.sum
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def total_self_ms
|
|
41
|
+
@total_self_ms ||= total_ms - children_ms
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def total_self_percentage(root_node = self)
|
|
45
|
+
@total_self_percentage ||= total_self_ms / root_node.children_ms * 100.0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def total_percentage(root_node = self)
|
|
49
|
+
@total_percentage ||= total_ms / root_node.children_ms * 100.0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def avg_self_ms
|
|
53
|
+
@avg_self_ms ||= total_self_ms / count
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_h(root_node = self) # rubocop:disable Metrics/MethodLength
|
|
57
|
+
@to_h ||= {
|
|
58
|
+
subject: subject,
|
|
59
|
+
count: count,
|
|
60
|
+
total_ms: total_ms,
|
|
61
|
+
total_percentage: total_percentage(root_node),
|
|
62
|
+
total_self_ms: total_self_ms,
|
|
63
|
+
total_self_percentage: total_self_percentage(root_node),
|
|
64
|
+
avg_ms: avg_ms,
|
|
65
|
+
avg_self_ms: avg_self_ms,
|
|
66
|
+
nested: children.map { |node| node.to_h(root_node) }
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_s(root_node = self)
|
|
71
|
+
@to_s ||= (base_text_list(root_node) + childeren_text_list(root_node)).join("\n")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# :reek:DuplicateMethodCall
|
|
75
|
+
# :reek:NestedIterators
|
|
76
|
+
def group_by_subject
|
|
77
|
+
@group_by_subject ||= (
|
|
78
|
+
[children.group_by(&:subject)] + children.map(&:group_by_subject)
|
|
79
|
+
).each_with_object({}) do |group, result|
|
|
80
|
+
group.each do |subject, nodes|
|
|
81
|
+
result[subject] ||= []
|
|
82
|
+
result[subject] += nodes
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def base_text_list(root_node) # rubocop:disable Metrics/MethodLength
|
|
90
|
+
[
|
|
91
|
+
'',
|
|
92
|
+
"- #{subject} -",
|
|
93
|
+
"called: #{count} time(s)",
|
|
94
|
+
"total execution time: #{total_ms.truncate(2)}ms",
|
|
95
|
+
"total percentage: #{total_percentage(root_node).truncate(2)}%",
|
|
96
|
+
"total self execution time: #{total_self_ms.truncate(2)}ms",
|
|
97
|
+
"total self percentage: #{total_self_percentage(root_node).truncate(2)}%",
|
|
98
|
+
"average execution time: #{avg_ms.truncate(2)}ms",
|
|
99
|
+
"average self execution time: #{avg_self_ms.truncate(2)}ms"
|
|
100
|
+
]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def childeren_text_list(root_node)
|
|
104
|
+
return [] if @children.empty?
|
|
105
|
+
|
|
106
|
+
children.map { |node| node.to_s(root_node) }
|
|
107
|
+
.join("\n")
|
|
108
|
+
.split("\n")
|
|
109
|
+
.map { |str| '| ' + str }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Flows
|
|
2
|
+
module Plugin
|
|
3
|
+
module Profiler
|
|
4
|
+
class Report
|
|
5
|
+
class Tree < Report
|
|
6
|
+
# @api private
|
|
7
|
+
class Node
|
|
8
|
+
attr_reader :subject
|
|
9
|
+
attr_reader :executions
|
|
10
|
+
|
|
11
|
+
def initialize(subject:)
|
|
12
|
+
@subject = subject
|
|
13
|
+
@children = {}
|
|
14
|
+
@cache = {}
|
|
15
|
+
|
|
16
|
+
@executions = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def [](subject)
|
|
20
|
+
@children[subject] ||= Node.new(subject: subject)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def children
|
|
24
|
+
@children.values
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def register_execution(microseconds)
|
|
28
|
+
@executions << microseconds
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Flows
|
|
2
|
+
module Plugin
|
|
3
|
+
module Profiler
|
|
4
|
+
# @api private
|
|
5
|
+
module Wrapper
|
|
6
|
+
class << self
|
|
7
|
+
def make_instance_wrapper(method_name) # rubocop:disable Metrics/MethodLength
|
|
8
|
+
Module.new.tap do |mod|
|
|
9
|
+
mod.define_method(method_name) do |*args, &block| # rubocop:disable Metrics/MethodLength
|
|
10
|
+
thread = Thread.current
|
|
11
|
+
klass = self.class
|
|
12
|
+
|
|
13
|
+
return super(*args, &block) unless thread[THREAD_VAR_FLAG]
|
|
14
|
+
|
|
15
|
+
report = thread[THREAD_VAR_REPORT]
|
|
16
|
+
report.add(:started, klass, :instance, method_name, nil)
|
|
17
|
+
|
|
18
|
+
before = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_microsecond)
|
|
19
|
+
super(*args, &block)
|
|
20
|
+
ensure
|
|
21
|
+
if before
|
|
22
|
+
after = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_microsecond)
|
|
23
|
+
report.add(:finished, klass, :instance, method_name, after - before)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def make_singleton_wrapper(method_name) # rubocop:disable Metrics/MethodLength
|
|
30
|
+
Module.new.tap do |mod|
|
|
31
|
+
mod.define_method(method_name) do |*args, &block| # rubocop:disable Metrics/MethodLength
|
|
32
|
+
thread = Thread.current
|
|
33
|
+
|
|
34
|
+
return super(*args, &block) unless thread[THREAD_VAR_FLAG]
|
|
35
|
+
|
|
36
|
+
report = thread[THREAD_VAR_REPORT]
|
|
37
|
+
report.add(:started, self, :singleton, method_name, nil)
|
|
38
|
+
|
|
39
|
+
before = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_microsecond)
|
|
40
|
+
super(*args, &block)
|
|
41
|
+
ensure
|
|
42
|
+
if before
|
|
43
|
+
after = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_microsecond)
|
|
44
|
+
report.add(:finished, self, :singleton, method_name, after - before)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
require_relative 'railway/errors'
|
|
2
|
+
require_relative 'railway/step'
|
|
3
|
+
require_relative 'railway/step_list'
|
|
4
|
+
require_relative 'railway/dsl'
|
|
5
|
+
|
|
6
|
+
module Flows
|
|
7
|
+
# Flows::Railway is an implementation of a Railway Programming pattern.
|
|
8
|
+
#
|
|
9
|
+
# You may read about this pattern in the following articles:
|
|
10
|
+
#
|
|
11
|
+
# * [Programming on rails: Railway Oriented Programming](http://sandordargo.com/blog/2017/09/27/railway_oriented_programming).
|
|
12
|
+
# It's not about Ruby on Rails.
|
|
13
|
+
# * [Railway Oriented Programming: A powerful Functional Programming pattern](https://medium.com/@naveenkumarmuguda/railway-oriented-programming-a-powerful-functional-programming-pattern-ab454e467f31)
|
|
14
|
+
# * [Railway Oriented Programming in Elixir with Pattern Matching on Function Level and Pipelining](https://medium.com/elixirlabs/railway-oriented-programming-in-elixir-with-pattern-matching-on-function-level-and-pipelining-e53972cede98)
|
|
15
|
+
#
|
|
16
|
+
# Let's review a simple task and solve it using {Flows::Railway}:
|
|
17
|
+
#
|
|
18
|
+
# * you have to get a user by ID
|
|
19
|
+
# * get all user's blog posts
|
|
20
|
+
# * and convert it to an array of HTML-strings
|
|
21
|
+
#
|
|
22
|
+
# In such situation, we have to implement three parts of our task and compose it into something we can call,
|
|
23
|
+
# for example, from a Rails controller.
|
|
24
|
+
# Also, the first and third steps may fail (user not found, conversion to HTML failed).
|
|
25
|
+
# And if a step failed - we have to return failure info immediately.
|
|
26
|
+
#
|
|
27
|
+
# class RenderUserBlogPosts < Flows::Railway
|
|
28
|
+
# step :fetch_user
|
|
29
|
+
# step :get_blog_posts
|
|
30
|
+
# step :convert_to_html
|
|
31
|
+
#
|
|
32
|
+
# def fetch_user(id:)
|
|
33
|
+
# user = User.find_by_id(id)
|
|
34
|
+
# user ? ok(user: user) : err(message: "User #{id} not found")
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# def get_blog_posts(user:)
|
|
38
|
+
# ok(posts: User.posts)
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# def convert_to_html(posts:)
|
|
42
|
+
# posts_html = post.map(&:text).map do |text|
|
|
43
|
+
# html = convert(text)
|
|
44
|
+
# return err(message: "cannot convert to html: #{text}")
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# ok(posts_html: posts_html)
|
|
48
|
+
# end
|
|
49
|
+
#
|
|
50
|
+
# private
|
|
51
|
+
#
|
|
52
|
+
# # returns String or nil
|
|
53
|
+
# def convert(text)
|
|
54
|
+
# # some implementation here
|
|
55
|
+
# end
|
|
56
|
+
# end
|
|
57
|
+
#
|
|
58
|
+
# RenderUserBlogPosts.call(id: 10)
|
|
59
|
+
# # result object returned
|
|
60
|
+
#
|
|
61
|
+
# Let's describe how it works.
|
|
62
|
+
#
|
|
63
|
+
# First of all you have to inherit your railway from `Flows::Railway`.
|
|
64
|
+
#
|
|
65
|
+
# Then you must define list of your steps using `step` DSL method.
|
|
66
|
+
# Steps will be executed in the given order.
|
|
67
|
+
#
|
|
68
|
+
# The you have to provide step implementations. It should be done by using
|
|
69
|
+
# public methods with the corresponding names.
|
|
70
|
+
# _Please write your step implementations in the step definition order._
|
|
71
|
+
# _It will make your railway easier to read by other engineers._
|
|
72
|
+
#
|
|
73
|
+
# Each step should return {Flows::Result} Object.
|
|
74
|
+
# If Result Object is successful - next step will be called or
|
|
75
|
+
# this object becomes a railway execution result in the case of last step.
|
|
76
|
+
# If Result Object is failure - this object becomes execution result immediately.
|
|
77
|
+
#
|
|
78
|
+
# Place all the helpers methods in the private section of the class.
|
|
79
|
+
#
|
|
80
|
+
# To help with writing methods {Flows::Result::Helpers} is already included.
|
|
81
|
+
#
|
|
82
|
+
# {Railway} is a very simple but not very flexible abstraction.
|
|
83
|
+
# It has a good performance and a small overhead.
|
|
84
|
+
#
|
|
85
|
+
# ## `Flows::Railway` execution rules
|
|
86
|
+
#
|
|
87
|
+
# * steps execution happens from the first to the last step
|
|
88
|
+
# * input arguments (`Railway#call(...)`) becomes the input of the first step
|
|
89
|
+
# * each step should return Result Object (`Flows::Result::Helpers` already included)
|
|
90
|
+
# * if step returns failed result - execution stops and failed Result Object returned from Railway
|
|
91
|
+
# * if step returns successful result - result data becomes arguments of the following step
|
|
92
|
+
# * if the last step returns successful result - it becomes a result of a Railway execution
|
|
93
|
+
#
|
|
94
|
+
# ## Step definitions
|
|
95
|
+
#
|
|
96
|
+
# Two ways of step definition exist. First is by using an instance method:
|
|
97
|
+
#
|
|
98
|
+
# step :do_something
|
|
99
|
+
#
|
|
100
|
+
# def do_something(**arguments)
|
|
101
|
+
# # some implementation
|
|
102
|
+
# # Result Object as return value
|
|
103
|
+
# end
|
|
104
|
+
#
|
|
105
|
+
# Second is by using lambda:
|
|
106
|
+
#
|
|
107
|
+
# step :do_something, ->(**arguments) { ok(some: 'data') }
|
|
108
|
+
#
|
|
109
|
+
# Definition with lambda exists for debugging/testing purposes, it has higher priority than method implementation.
|
|
110
|
+
# _Do not use lambda implementations for your business logic!_
|
|
111
|
+
#
|
|
112
|
+
# __Think about Railway as about small book: you have a "table of contents"
|
|
113
|
+
# in a form of step definitions and actual "chapters" in the same order
|
|
114
|
+
# in a form of public methods. And your private methods becomes something like "appendix".__
|
|
115
|
+
#
|
|
116
|
+
# ## Advanced initialization
|
|
117
|
+
#
|
|
118
|
+
# In a simple case you can just invoke `YourRailway.call(..)`. Under the hood it works like `.new.call(...)`,
|
|
119
|
+
# but `.new` part will be executed ones and memoized ({Flows::Plugin::ImplicitInit} included).
|
|
120
|
+
#
|
|
121
|
+
# You can include {Flows::Plugin::DependencyInjector} into your Railway and in this case you will
|
|
122
|
+
# need to do `.new(...).call` manually.
|
|
123
|
+
class Railway
|
|
124
|
+
extend ::Flows::Plugin::ImplicitInit
|
|
125
|
+
|
|
126
|
+
include ::Flows::Result::Helpers
|
|
127
|
+
extend ::Flows::Result::Helpers
|
|
128
|
+
|
|
129
|
+
extend DSL
|
|
130
|
+
|
|
131
|
+
def initialize
|
|
132
|
+
klass = self.class
|
|
133
|
+
steps = klass.steps
|
|
134
|
+
|
|
135
|
+
raise NoStepsError, klass if steps.empty?
|
|
136
|
+
|
|
137
|
+
@__flows_railway_flow = Flows::Flow.new(
|
|
138
|
+
start_node: steps.first_step_name,
|
|
139
|
+
node_map: steps.to_node_map(self)
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Executes Railway with provided keyword arguments, returns Result Object
|
|
144
|
+
#
|
|
145
|
+
# @return [Flows::Result]
|
|
146
|
+
def call(**kwargs)
|
|
147
|
+
context = {}
|
|
148
|
+
|
|
149
|
+
@__flows_railway_flow.call(ok(**kwargs), context: context).tap do |result|
|
|
150
|
+
result.meta[:last_step] = context[:last_step]
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|