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
@@ -22,6 +22,7 @@ module Flows
22
22
  #
23
23
  # * `success_with(status, &block)` - defines contract for a successful result with status `status`.
24
24
  # * `failure_with(status, &block)` - defines contract for a failure result with status `status`.
25
+ # * `skip_output_contract` - disables contract check and transformation for current class and children.
25
26
  #
26
27
  # @example with one possible output contract
27
28
  # class DoJob
@@ -0,0 +1,35 @@
1
+ module Flows
2
+ module Plugin
3
+ module Profiler
4
+ # @api private
5
+ module Injector
6
+ class << self
7
+ def make_module(method_name)
8
+ Module.new.tap do |mod|
9
+ add_included(mod, method_name)
10
+ add_extended(mod, method_name)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def add_included(mod, method_name)
17
+ mod.define_method(:included) do |target|
18
+ raise 'must be included into class' unless target.is_a?(Class)
19
+
20
+ target.prepend Wrapper.make_instance_wrapper(method_name)
21
+ end
22
+ end
23
+
24
+ def add_extended(mod, method_name)
25
+ mod.define_method(:extended) do |target|
26
+ raise 'must be extended into class' unless target.is_a?(Class)
27
+
28
+ target.singleton_class.prepend(Wrapper.make_singleton_wrapper(method_name))
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,43 @@
1
+ module Flows
2
+ module Plugin
3
+ module Profiler
4
+ class Report
5
+ # @api private
6
+ Event = Struct.new(:method_class, :method_type, :method_name, :data) do
7
+ def subject
8
+ "#{method_class}#{delimeter}#{method_name}"
9
+ end
10
+
11
+ private
12
+
13
+ def delimeter
14
+ case method_type
15
+ when :instance then '#'
16
+ when :singleton then '.'
17
+ end
18
+ end
19
+ end
20
+
21
+ # @api private
22
+ #
23
+ # Method execution start event.
24
+ class StartEvent < Event
25
+ def to_s
26
+ "start: #{subject}"
27
+ end
28
+ end
29
+
30
+ # @api private
31
+ #
32
+ # Method execution finish event.
33
+ #
34
+ # Data is an execution time in microseconds.
35
+ class FinishEvent < Event
36
+ def to_s
37
+ "finish(#{data} microseconds): #{subject}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -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,41 @@
1
+ require_relative 'flat/method_report'
2
+
3
+ module Flows
4
+ module Plugin
5
+ module Profiler
6
+ class Report
7
+ # Flat report. Merges similar calls, hides execution structure.
8
+ #
9
+ # It's a variation of a {Rport::Tree} where all calls of the same method
10
+ # are combined into a one first-level entry.
11
+ #
12
+ # @example
13
+ # Flows::Plugin::Profiler.profile(:flat) do
14
+ # # some code here
15
+ # end
16
+ #
17
+ # puts Flows::Plugin::Profiler.last_report
18
+ class Flat < Tree
19
+ def to_a
20
+ method_reports.map(&:to_h)
21
+ end
22
+
23
+ def to_s
24
+ method_reports.map(&:to_s).join("\n")
25
+ end
26
+
27
+ private
28
+
29
+ def method_reports
30
+ @method_reports ||= root_calculated_node
31
+ .group_by_subject
32
+ .values
33
+ .map { |nodes| MethodReport.new(root_calculated_node, *nodes) }
34
+ .sort_by(&:total_self_ms)
35
+ .reverse
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ module Flows
2
+ module Plugin
3
+ module Profiler
4
+ class Report
5
+ # Raw report. Preserves events as is.
6
+ class Raw < Report
7
+ # @see Report#to_s
8
+ def to_s
9
+ raw_data.map(&:to_s).join("\n")
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ 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,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