flows 0.1.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +38 -0
  3. data/.gitignore +9 -1
  4. data/.mdlrc +1 -0
  5. data/.reek.yml +54 -0
  6. data/.rubocop.yml +44 -2
  7. data/.ruby-version +1 -1
  8. data/.yardopts +1 -0
  9. data/CHANGELOG.md +65 -0
  10. data/README.md +186 -256
  11. data/Rakefile +35 -1
  12. data/bin/.rubocop.yml +5 -0
  13. data/bin/all_the_errors +55 -0
  14. data/bin/benchmark +69 -78
  15. data/bin/benchmark_cli/compare.rb +118 -0
  16. data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
  17. data/bin/benchmark_cli/compare/base.rb +45 -0
  18. data/bin/benchmark_cli/compare/command.rb +47 -0
  19. data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
  20. data/bin/benchmark_cli/examples.rb +23 -0
  21. data/bin/benchmark_cli/examples/.rubocop.yml +19 -0
  22. data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
  23. data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
  24. data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
  25. data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
  26. data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
  27. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
  28. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
  29. data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
  30. data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
  31. data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
  32. data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
  33. data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
  34. data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
  35. data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
  36. data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
  37. data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
  38. data/bin/benchmark_cli/helpers.rb +12 -0
  39. data/bin/benchmark_cli/ruby.rb +15 -0
  40. data/bin/benchmark_cli/ruby/command.rb +38 -0
  41. data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
  42. data/bin/benchmark_cli/ruby/self_class.rb +69 -0
  43. data/bin/benchmark_cli/ruby/structs.rb +90 -0
  44. data/bin/console +1 -0
  45. data/bin/docserver +7 -0
  46. data/bin/errors +130 -0
  47. data/bin/errors_cli/contract_error_demo.rb +49 -0
  48. data/bin/errors_cli/di_error_demo.rb +38 -0
  49. data/bin/errors_cli/flow_error_demo.rb +22 -0
  50. data/bin/errors_cli/flows_router_error_demo.rb +15 -0
  51. data/bin/errors_cli/oc_error_demo.rb +40 -0
  52. data/bin/errors_cli/railway_error_demo.rb +10 -0
  53. data/bin/errors_cli/result_error_demo.rb +13 -0
  54. data/bin/errors_cli/scp_error_demo.rb +17 -0
  55. data/docs/.nojekyll +0 -0
  56. data/docs/README.md +13 -0
  57. data/docs/_sidebar.md +2 -0
  58. data/docs/index.html +30 -0
  59. data/flows.gemspec +27 -2
  60. data/forspell.dict +17 -0
  61. data/lefthook.yml +21 -0
  62. data/lib/flows.rb +13 -5
  63. data/lib/flows/contract.rb +402 -0
  64. data/lib/flows/contract/array.rb +55 -0
  65. data/lib/flows/contract/case_eq.rb +43 -0
  66. data/lib/flows/contract/compose.rb +77 -0
  67. data/lib/flows/contract/either.rb +53 -0
  68. data/lib/flows/contract/error.rb +25 -0
  69. data/lib/flows/contract/hash.rb +75 -0
  70. data/lib/flows/contract/hash_of.rb +70 -0
  71. data/lib/flows/contract/helpers.rb +22 -0
  72. data/lib/flows/contract/predicate.rb +34 -0
  73. data/lib/flows/contract/transformer.rb +50 -0
  74. data/lib/flows/contract/tuple.rb +70 -0
  75. data/lib/flows/flow.rb +96 -7
  76. data/lib/flows/flow/errors.rb +29 -0
  77. data/lib/flows/flow/node.rb +132 -0
  78. data/lib/flows/flow/router.rb +29 -0
  79. data/lib/flows/flow/router/custom.rb +59 -0
  80. data/lib/flows/flow/router/errors.rb +11 -0
  81. data/lib/flows/flow/router/simple.rb +25 -0
  82. data/lib/flows/plugin.rb +14 -0
  83. data/lib/flows/plugin/dependency_injector.rb +159 -0
  84. data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
  85. data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
  86. data/lib/flows/plugin/dependency_injector/dependency_list.rb +57 -0
  87. data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
  88. data/lib/flows/plugin/implicit_init.rb +45 -0
  89. data/lib/flows/plugin/output_contract.rb +85 -0
  90. data/lib/flows/plugin/output_contract/dsl.rb +48 -0
  91. data/lib/flows/plugin/output_contract/errors.rb +74 -0
  92. data/lib/flows/plugin/output_contract/wrapper.rb +55 -0
  93. data/lib/flows/plugin/profiler.rb +114 -0
  94. data/lib/flows/plugin/profiler/injector.rb +35 -0
  95. data/lib/flows/plugin/profiler/report.rb +48 -0
  96. data/lib/flows/plugin/profiler/report/events.rb +43 -0
  97. data/lib/flows/plugin/profiler/report/flat.rb +41 -0
  98. data/lib/flows/plugin/profiler/report/flat/method_report.rb +81 -0
  99. data/lib/flows/plugin/profiler/report/raw.rb +15 -0
  100. data/lib/flows/plugin/profiler/report/tree.rb +98 -0
  101. data/lib/flows/plugin/profiler/report/tree/calculated_node.rb +116 -0
  102. data/lib/flows/plugin/profiler/report/tree/node.rb +35 -0
  103. data/lib/flows/plugin/profiler/wrapper.rb +53 -0
  104. data/lib/flows/railway.rb +154 -0
  105. data/lib/flows/railway/dsl.rb +18 -0
  106. data/lib/flows/railway/errors.rb +17 -0
  107. data/lib/flows/railway/step.rb +24 -0
  108. data/lib/flows/railway/step_list.rb +38 -0
  109. data/lib/flows/result.rb +189 -2
  110. data/lib/flows/result/do.rb +172 -0
  111. data/lib/flows/result/err.rb +12 -6
  112. data/lib/flows/result/errors.rb +29 -17
  113. data/lib/flows/result/helpers.rb +25 -3
  114. data/lib/flows/result/ok.rb +12 -6
  115. data/lib/flows/shared_context_pipeline.rb +299 -0
  116. data/lib/flows/shared_context_pipeline/dsl.rb +12 -0
  117. data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +38 -0
  118. data/lib/flows/shared_context_pipeline/dsl/tracks.rb +52 -0
  119. data/lib/flows/shared_context_pipeline/errors.rb +17 -0
  120. data/lib/flows/shared_context_pipeline/mutation_step.rb +29 -0
  121. data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
  122. data/lib/flows/shared_context_pipeline/step.rb +44 -0
  123. data/lib/flows/shared_context_pipeline/track.rb +54 -0
  124. data/lib/flows/shared_context_pipeline/track_list.rb +51 -0
  125. data/lib/flows/shared_context_pipeline/wrap.rb +74 -0
  126. data/lib/flows/util.rb +17 -0
  127. data/lib/flows/util/inheritable_singleton_vars.rb +86 -0
  128. data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +98 -0
  129. data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +91 -0
  130. data/lib/flows/util/prepend_to_class.rb +179 -0
  131. data/lib/flows/version.rb +1 -1
  132. metadata +288 -20
  133. data/.travis.yml +0 -8
  134. data/Gemfile.lock +0 -119
  135. data/bin/demo +0 -66
  136. data/bin/examples.rb +0 -159
  137. data/bin/profile_10steps +0 -64
  138. data/bin/ruby_benchmarks +0 -26
  139. data/lib/flows/node.rb +0 -27
  140. data/lib/flows/operation.rb +0 -54
  141. data/lib/flows/operation/builder.rb +0 -130
  142. data/lib/flows/operation/builder/build_router.rb +0 -37
  143. data/lib/flows/operation/dsl.rb +0 -72
  144. data/lib/flows/operation/errors.rb +0 -75
  145. data/lib/flows/operation/executor.rb +0 -78
  146. data/lib/flows/result_router.rb +0 -14
  147. data/lib/flows/router.rb +0 -22
@@ -0,0 +1,48 @@
1
+ module Flows
2
+ module Plugin
3
+ module OutputContract
4
+ # DSL for OutputContract plugin.
5
+ module DSL
6
+ # Hash of contracts for successful results.
7
+ attr_reader :success_contracts
8
+
9
+ # Hash of contracts for failure results.
10
+ attr_reader :failure_contracts
11
+
12
+ # Is contract check and transformation disabled
13
+ attr_reader :skip_output_contract_flag
14
+
15
+ SingletonVarsSetup = Flows::Util::InheritableSingletonVars::DupStrategy.make_module(
16
+ '@success_contracts' => {},
17
+ '@failure_contracts' => {},
18
+ '@skip_output_contract_flag' => false
19
+ )
20
+
21
+ include SingletonVarsSetup
22
+
23
+ # Defines a contract for a successful result with specific status.
24
+ #
25
+ # @param status [Symbol] Corresponding result status.
26
+ # @param contract_block [Proc] This block will be passed to {Contract.make} to get a contract.
27
+ def success_with(status, &contract_block)
28
+ success_contracts[status] = Flows::Contract.make(&contract_block)
29
+ end
30
+
31
+ # Defines a contract for a failure result with specific status.
32
+ #
33
+ # @param status [Symbol] Corresponding result status.
34
+ # @param contract_block [Proc] This block will be passed to {Contract.make} to get a contract.
35
+ def failure_with(status, &contract_block)
36
+ failure_contracts[status] = Flows::Contract.make(&contract_block)
37
+ end
38
+
39
+ # Disables contract check and transformation for current class and children.
40
+ #
41
+ # @param enable [Boolean] if true - contracts are disabled
42
+ def skip_output_contract(enable = true)
43
+ @skip_output_contract_flag = enable
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,74 @@
1
+ module Flows
2
+ module Plugin
3
+ module OutputContract
4
+ # Base error class for output contract errors.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when no single contract for successful results is defined
8
+ class NoContractError < Error
9
+ def initialize(klass)
10
+ @klass = klass
11
+ end
12
+
13
+ def message
14
+ "No single success contract defined for #{@klass}"
15
+ end
16
+ end
17
+
18
+ # Raised when result's data violates contract
19
+ class ContractError < Error
20
+ def initialize(klass, result, error)
21
+ @klass = klass
22
+ @result = result
23
+ @error = error
24
+ end
25
+
26
+ def message
27
+ shifted_error = @error.split("\n").map { |str| ' ' + str }.join("\n")
28
+
29
+ "Output contract for #{@klass} is violated.\n" \
30
+ "Result:\n" \
31
+ " `#{@result.inspect}`\n" \
32
+ "Contract Error:\n" \
33
+ "#{shifted_error}"
34
+ end
35
+ end
36
+
37
+ # Raised when no contract found for result
38
+ class StatusError < Error
39
+ def initialize(klass, result, allowed_statuses)
40
+ @klass = klass
41
+ @result = result
42
+ @allowed_statuses = allowed_statuses
43
+ end
44
+
45
+ def message
46
+ allowed_statuses_str = @allowed_statuses.map { |st| "`#{st.inspect}`" }.join(', ')
47
+
48
+ "Output contract for #{@klass} is violated.\n" \
49
+ "Result:\n" \
50
+ " `#{@result.inspect}`\n" \
51
+ "Contract Error:\n" \
52
+ " has unexpected status `#{@result.status.inspect}`\n" \
53
+ " allowed statuses for `#{@result.class}` are: #{allowed_statuses_str}"
54
+ end
55
+ end
56
+
57
+ # Raised when not a result object returned
58
+ class ResultTypeError < Error
59
+ def initialize(klass, result)
60
+ @klass = klass
61
+ @result = result
62
+ end
63
+
64
+ def message
65
+ "Output contract for #{@klass} is violated.\n" \
66
+ "Result:\n" \
67
+ " `#{@result.inspect}`\n" \
68
+ "Contract Error:\n" \
69
+ ' result must be instance of `Flows::Result`'
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,55 @@
1
+ module Flows
2
+ module Plugin
3
+ module OutputContract
4
+ # Contains wrappers for initializer and `#call` methods.
5
+ #
6
+ # @api private
7
+ module Wrapper
8
+ def initialize(*args, &block)
9
+ super(*args, &block)
10
+ klass = self.class
11
+ raise NoContractError, klass if klass.success_contracts.empty? && !klass.skip_output_contract_flag
12
+ end
13
+
14
+ def call(*args, &block)
15
+ result = super(*args, &block)
16
+ klass = self.class
17
+
18
+ return result if klass.skip_output_contract_flag
19
+
20
+ Util.transform_result(klass, result)
21
+
22
+ result
23
+ end
24
+
25
+ # Helper methods for {Wrapper} are extracted to this
26
+ # module as singleton methods to not pollute user classes.
27
+ #
28
+ # @api private
29
+ module Util
30
+ class << self
31
+ def transform_result(klass, result)
32
+ contract = Util.contract_for(klass, result)
33
+
34
+ data = result.send(:data)
35
+
36
+ transformed_result = contract.transform(data)
37
+ raise ContractError.new(klass, result, transformed_result.error) if transformed_result.err?
38
+
39
+ result.send(:'data=', transformed_result.unwrap)
40
+ end
41
+
42
+ def contract_for(klass, result)
43
+ raise ResultTypeError.new(klass, result) unless result.is_a?(Flows::Result)
44
+
45
+ status = result.status
46
+ contracts = result.ok? ? klass.success_contracts : klass.failure_contracts
47
+
48
+ contracts[status] || raise(StatusError.new(klass, result, contracts.keys))
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,114 @@
1
+ require_relative 'profiler/report'
2
+ require_relative 'profiler/injector'
3
+ require_relative 'profiler/wrapper'
4
+
5
+ module Flows
6
+ module Plugin
7
+ # Allows to record execution count and time of particular method on class or singleton class.
8
+ #
9
+ # Recorded data can be displayed in a different ways.
10
+ # See {Profiler::Report} implementations for possible options.
11
+ #
12
+ # To do a measurement you have call your classes inside {.profile} block.
13
+ #
14
+ # @note even without calling {.profile} using this module has some performance
15
+ # impact. Don't left this module used in production environments.
16
+ #
17
+ # @example
18
+ # class MyClass
19
+ # CallProfiler = Flows::Plugin::Profiler.for_method(:call)
20
+ # include CallProfiler
21
+ #
22
+ # def call(a, b)
23
+ # # some work here
24
+ # end
25
+ # end
26
+ #
27
+ # class AnotherClass
28
+ # CallProfiler = Flows::Plugin::Profiler.for_method(:perform)
29
+ # extend CallProfiler
30
+ #
31
+ # def self.perform(x)
32
+ # MyClass.new.call(x, x)
33
+ # end
34
+ # end
35
+ #
36
+ # last_result = Flows::Plugin::Profiler.profile do
37
+ # AnotherClass.perform(2)
38
+ # AnotherClass.perform(6)
39
+ # end
40
+ #
41
+ # Profiler.last_report.to_a
42
+ # # => [
43
+ # # [:started, AnotherClass, :singleton, :perform, nil],
44
+ # # [:started, MyClass, :instance, :call, nil],
45
+ # # [:finished, MyClass, :instance, :call, 7.3],
46
+ # # [:finished, AnotherClass, :singleton, :perform, 10.5],
47
+ # # [:started, AnotherClass, :singleton, :perform, nil],
48
+ # # [:started, MyClass, :instance, :call, nil],
49
+ # # [:finished, MyClass, :instance, :call, 8.8],
50
+ # # [:finished, AnotherClass, :singleton, :perform, 14.2]
51
+ # # ]
52
+ module Profiler
53
+ THREAD_VAR_FLAG = :flows_profiler_flag
54
+ THREAD_VAR_REPORT = :flows_profiler_report
55
+
56
+ class << self
57
+ # Generates profiler module for a particular method.
58
+ #
59
+ # Use `include` for instance methods and `extend` for singleton ones.
60
+ #
61
+ # @param method_name [Symbol] method to wrap with profiling.
62
+ # @return [Module] module to include or extend.
63
+ def for_method(method_name)
64
+ Module.new.tap do |mod|
65
+ injector_mod = Injector.make_module(method_name)
66
+ mod.const_set(:Injector, injector_mod)
67
+ mod.extend injector_mod
68
+ end
69
+ end
70
+
71
+ # Profiles a block execution.
72
+ #
73
+ # @param report [Report, Symbol]
74
+ # desired {Report} to be used.
75
+ # In case of symbol `:some_name` the `Flows::Plugin::Profiler::Report::SomeName.new` will be used.
76
+ # @yield code to profile
77
+ # @return block result
78
+ def profile(report = :raw)
79
+ thread = Thread.current
80
+
81
+ thread[THREAD_VAR_FLAG] = true
82
+ thread[THREAD_VAR_REPORT] = make_report(report)
83
+
84
+ yield
85
+ ensure
86
+ thread[THREAD_VAR_FLAG] = false
87
+ end
88
+
89
+ # Resets thread-local variables used for reporting.
90
+ def reset
91
+ thread = Thread.current
92
+ thread[THREAD_VAR_FLAG] = false
93
+ thread[THREAD_VAR_REPORT] = nil
94
+ end
95
+
96
+ # @return [Report, nil] last generated report if some.
97
+ def last_report
98
+ Thread.current[THREAD_VAR_REPORT]
99
+ end
100
+
101
+ private
102
+
103
+ def make_report(report_or_sym)
104
+ case report_or_sym
105
+ when Report then report_or_sym
106
+ when Symbol
107
+ const_name = report_or_sym.to_s.split('_').map(&:capitalize).join
108
+ Report.const_get(const_name).new
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -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,48 @@
1
+ require_relative 'report/events'
2
+ require_relative 'report/raw'
3
+ require_relative 'report/tree'
4
+ require_relative 'report/flat'
5
+
6
+ module Flows
7
+ module Plugin
8
+ module Profiler
9
+ # Base class for {Profiler} reports.
10
+ #
11
+ # @!method to_s
12
+ # @abstract
13
+ # @return [String] human-readable representation.
14
+ class Report
15
+ # @return [Array<Array>] raw profiler events
16
+ attr_reader :raw_data
17
+
18
+ def initialize
19
+ @raw_data = []
20
+ end
21
+
22
+ # Add event to profile report.
23
+ #
24
+ # @param event_type [:started, :finished] event type
25
+ # @param klass [Class] class where called method is placed
26
+ # @param method_type [:instance, :singleton] method type
27
+ # @param method_name [Symbol] name of the called method
28
+ # @param data [nil, Float] event data, time represented as
29
+ # a Float microseconds value.
30
+ def add(*args)
31
+ raw_data << args
32
+ end
33
+
34
+ # @return [Array<Event>] array of events
35
+ def events
36
+ raw_data.map do |raw_event|
37
+ klass = case raw_event.first
38
+ when :started then StartEvent
39
+ when :finished then FinishEvent
40
+ end
41
+
42
+ klass.new(*raw_event[1..-1])
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ 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,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