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.
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,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,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,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