flows 0.2.0 → 0.6.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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/{build.yml → test.yml} +5 -10
  3. data/.gitignore +9 -1
  4. data/.mdlrc +1 -1
  5. data/.reek.yml +54 -0
  6. data/.rubocop.yml +26 -7
  7. data/.rubocop_todo.yml +27 -0
  8. data/.ruby-version +1 -1
  9. data/.yardopts +1 -0
  10. data/CHANGELOG.md +81 -0
  11. data/Gemfile +0 -6
  12. data/README.md +167 -363
  13. data/Rakefile +35 -1
  14. data/bin/.rubocop.yml +5 -0
  15. data/bin/all_the_errors +55 -0
  16. data/bin/benchmark +73 -105
  17. data/bin/benchmark_cli/compare.rb +118 -0
  18. data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
  19. data/bin/benchmark_cli/compare/base.rb +45 -0
  20. data/bin/benchmark_cli/compare/command.rb +47 -0
  21. data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
  22. data/bin/benchmark_cli/examples.rb +23 -0
  23. data/bin/benchmark_cli/examples/.rubocop.yml +22 -0
  24. data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
  25. data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
  26. data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
  27. data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
  28. data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
  29. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
  30. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
  31. data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
  32. data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
  33. data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
  34. data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
  35. data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
  36. data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
  37. data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
  38. data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
  39. data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
  40. data/bin/benchmark_cli/helpers.rb +12 -0
  41. data/bin/benchmark_cli/ruby.rb +15 -0
  42. data/bin/benchmark_cli/ruby/command.rb +38 -0
  43. data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
  44. data/bin/benchmark_cli/ruby/self_class.rb +69 -0
  45. data/bin/benchmark_cli/ruby/structs.rb +90 -0
  46. data/bin/console +1 -0
  47. data/bin/docserver +7 -0
  48. data/bin/errors +138 -0
  49. data/bin/errors_cli/contract_error_demo.rb +49 -0
  50. data/bin/errors_cli/di_error_demo.rb +38 -0
  51. data/bin/errors_cli/flow_error_demo.rb +22 -0
  52. data/bin/errors_cli/flows_router_error_demo.rb +15 -0
  53. data/bin/errors_cli/interface_error_demo.rb +17 -0
  54. data/bin/errors_cli/oc_error_demo.rb +40 -0
  55. data/bin/errors_cli/railway_error_demo.rb +10 -0
  56. data/bin/errors_cli/result_error_demo.rb +13 -0
  57. data/bin/errors_cli/scp_error_demo.rb +17 -0
  58. data/docs/README.md +3 -187
  59. data/docs/_sidebar.md +0 -24
  60. data/docs/index.html +1 -1
  61. data/flows.gemspec +27 -2
  62. data/forspell.dict +9 -0
  63. data/lefthook.yml +9 -0
  64. data/lib/flows.rb +11 -5
  65. data/lib/flows/contract.rb +402 -0
  66. data/lib/flows/contract/array.rb +55 -0
  67. data/lib/flows/contract/case_eq.rb +43 -0
  68. data/lib/flows/contract/compose.rb +77 -0
  69. data/lib/flows/contract/either.rb +53 -0
  70. data/lib/flows/contract/error.rb +24 -0
  71. data/lib/flows/contract/hash.rb +75 -0
  72. data/lib/flows/contract/hash_of.rb +70 -0
  73. data/lib/flows/contract/helpers.rb +22 -0
  74. data/lib/flows/contract/predicate.rb +34 -0
  75. data/lib/flows/contract/transformer.rb +50 -0
  76. data/lib/flows/contract/tuple.rb +70 -0
  77. data/lib/flows/flow.rb +96 -7
  78. data/lib/flows/flow/errors.rb +29 -0
  79. data/lib/flows/flow/node.rb +132 -0
  80. data/lib/flows/flow/router.rb +29 -0
  81. data/lib/flows/flow/router/custom.rb +59 -0
  82. data/lib/flows/flow/router/errors.rb +11 -0
  83. data/lib/flows/flow/router/simple.rb +25 -0
  84. data/lib/flows/plugin.rb +15 -0
  85. data/lib/flows/plugin/dependency_injector.rb +170 -0
  86. data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
  87. data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
  88. data/lib/flows/plugin/dependency_injector/dependency_list.rb +55 -0
  89. data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
  90. data/lib/flows/plugin/implicit_init.rb +45 -0
  91. data/lib/flows/plugin/interface.rb +84 -0
  92. data/lib/flows/plugin/output_contract.rb +85 -0
  93. data/lib/flows/plugin/output_contract/dsl.rb +48 -0
  94. data/lib/flows/plugin/output_contract/errors.rb +74 -0
  95. data/lib/flows/plugin/output_contract/wrapper.rb +55 -0
  96. data/lib/flows/plugin/profiler.rb +114 -0
  97. data/lib/flows/plugin/profiler/injector.rb +35 -0
  98. data/lib/flows/plugin/profiler/report.rb +48 -0
  99. data/lib/flows/plugin/profiler/report/events.rb +43 -0
  100. data/lib/flows/plugin/profiler/report/flat.rb +41 -0
  101. data/lib/flows/plugin/profiler/report/flat/method_report.rb +80 -0
  102. data/lib/flows/plugin/profiler/report/raw.rb +15 -0
  103. data/lib/flows/plugin/profiler/report/tree.rb +98 -0
  104. data/lib/flows/plugin/profiler/report/tree/calculated_node.rb +116 -0
  105. data/lib/flows/plugin/profiler/report/tree/node.rb +34 -0
  106. data/lib/flows/plugin/profiler/wrapper.rb +53 -0
  107. data/lib/flows/railway.rb +140 -34
  108. data/lib/flows/railway/dsl.rb +8 -18
  109. data/lib/flows/railway/errors.rb +8 -12
  110. data/lib/flows/railway/step.rb +24 -0
  111. data/lib/flows/railway/step_list.rb +38 -0
  112. data/lib/flows/result.rb +188 -2
  113. data/lib/flows/result/do.rb +158 -16
  114. data/lib/flows/result/err.rb +12 -6
  115. data/lib/flows/result/errors.rb +29 -17
  116. data/lib/flows/result/helpers.rb +25 -3
  117. data/lib/flows/result/ok.rb +12 -6
  118. data/lib/flows/shared_context_pipeline.rb +342 -0
  119. data/lib/flows/shared_context_pipeline/dsl.rb +12 -0
  120. data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +35 -0
  121. data/lib/flows/shared_context_pipeline/dsl/tracks.rb +52 -0
  122. data/lib/flows/shared_context_pipeline/errors.rb +17 -0
  123. data/lib/flows/shared_context_pipeline/mutation_step.rb +30 -0
  124. data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
  125. data/lib/flows/shared_context_pipeline/step.rb +55 -0
  126. data/lib/flows/shared_context_pipeline/track.rb +54 -0
  127. data/lib/flows/shared_context_pipeline/track_list.rb +51 -0
  128. data/lib/flows/shared_context_pipeline/wrap.rb +73 -0
  129. data/lib/flows/util.rb +17 -0
  130. data/lib/flows/util/inheritable_singleton_vars.rb +86 -0
  131. data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +100 -0
  132. data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +91 -0
  133. data/lib/flows/util/prepend_to_class.rb +191 -0
  134. data/lib/flows/version.rb +1 -1
  135. metadata +253 -38
  136. data/Gemfile.lock +0 -174
  137. data/bin/demo +0 -66
  138. data/bin/examples.rb +0 -195
  139. data/bin/profile_10steps +0 -106
  140. data/bin/ruby_benchmarks +0 -26
  141. data/docs/CNAME +0 -1
  142. data/docs/contributing/benchmarks_profiling.md +0 -3
  143. data/docs/contributing/local_development.md +0 -3
  144. data/docs/flow/direct_usage.md +0 -3
  145. data/docs/flow/general_idea.md +0 -3
  146. data/docs/operation/basic_usage.md +0 -1
  147. data/docs/operation/inject_steps.md +0 -3
  148. data/docs/operation/lambda_steps.md +0 -3
  149. data/docs/operation/result_shapes.md +0 -3
  150. data/docs/operation/routing_tracks.md +0 -3
  151. data/docs/operation/wrapping_steps.md +0 -3
  152. data/docs/overview/performance.md +0 -336
  153. data/docs/railway/basic_usage.md +0 -232
  154. data/docs/result_objects/basic_usage.md +0 -196
  155. data/docs/result_objects/do_notation.md +0 -139
  156. data/lib/flows/node.rb +0 -27
  157. data/lib/flows/operation.rb +0 -52
  158. data/lib/flows/operation/builder.rb +0 -130
  159. data/lib/flows/operation/builder/build_router.rb +0 -37
  160. data/lib/flows/operation/dsl.rb +0 -93
  161. data/lib/flows/operation/errors.rb +0 -75
  162. data/lib/flows/operation/executor.rb +0 -78
  163. data/lib/flows/railway/builder.rb +0 -68
  164. data/lib/flows/railway/executor.rb +0 -23
  165. data/lib/flows/result_router.rb +0 -14
  166. data/lib/flows/router.rb +0 -22
@@ -1,48 +1,154 @@
1
- require_relative './railway/errors'
2
-
3
- require_relative './railway/dsl'
4
- require_relative './railway/builder'
5
- require_relative './railway/executor'
1
+ require_relative 'railway/errors'
2
+ require_relative 'railway/step'
3
+ require_relative 'railway/step_list'
4
+ require_relative 'railway/dsl'
6
5
 
7
6
  module Flows
8
- # Railway DSL
9
- module Railway
10
- def self.included(mod)
11
- mod.extend ::Flows::Railway::DSL
12
- end
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
13
125
 
14
126
  include ::Flows::Result::Helpers
127
+ extend ::Flows::Result::Helpers
15
128
 
16
- def initialize(method_source: nil, deps: {})
17
- _flows_do_checks
18
-
19
- flow = _flows_make_flow(method_source || self, deps)
20
- @_flows_executor = _flows_make_executor(flow)
21
- end
129
+ extend DSL
22
130
 
23
- def call(**params)
24
- @_flows_executor.call(**params)
25
- end
131
+ def initialize
132
+ klass = self.class
133
+ steps = klass.steps
26
134
 
27
- private
135
+ raise NoStepsError, klass if steps.empty?
28
136
 
29
- def _flows_do_checks
30
- raise NoStepsError if self.class.steps.empty?
137
+ @__flows_railway_flow = Flows::Flow.new(
138
+ start_node: steps.first_step_name,
139
+ node_map: steps.to_node_map(self)
140
+ )
31
141
  end
32
142
 
33
- def _flows_make_flow(method_source, deps)
34
- ::Flows::Railway::Builder.new(
35
- steps: self.class.steps,
36
- method_source: method_source,
37
- deps: deps
38
- ).call
39
- end
143
+ # Executes Railway with provided keyword arguments, returns Result Object
144
+ #
145
+ # @return [Flows::Result]
146
+ def call(**kwargs)
147
+ context = {}
40
148
 
41
- def _flows_make_executor(flow)
42
- ::Flows::Railway::Executor.new(
43
- flow: flow,
44
- class_name: self.class.name
45
- )
149
+ @__flows_railway_flow.call(ok(**kwargs), context: context).tap do |result|
150
+ result.meta[:last_step] = context[:last_step]
151
+ end
46
152
  end
47
153
  end
48
154
  end
@@ -1,27 +1,17 @@
1
1
  module Flows
2
- module Railway
3
- # DSL methods for Railway
2
+ class Railway
3
+ # @api private
4
4
  module DSL
5
5
  attr_reader :steps
6
6
 
7
- def self.extended(mod, steps = nil)
8
- mod.instance_variable_set(:@steps, steps || [])
7
+ SingletonVarsSetup = Flows::Util::InheritableSingletonVars::DupStrategy.make_module(
8
+ '@steps' => StepList.new
9
+ )
9
10
 
10
- mod.class_exec do
11
- def self.inherited(subclass)
12
- ::Flows::Railway::DSL.extended(subclass, steps.map(&:dup))
13
- super
14
- end
15
- end
16
- end
17
-
18
- include Flows::Result::Helpers
11
+ include SingletonVarsSetup
19
12
 
20
- def step(name, custom_body = nil)
21
- @steps << {
22
- name: name,
23
- custom_body: custom_body
24
- }
13
+ def step(name, lambda = nil)
14
+ steps.add(name: name, lambda: lambda)
25
15
  end
26
16
  end
27
17
  end
@@ -1,21 +1,17 @@
1
1
  module Flows
2
- module Railway
3
- # rubocop:disable Style/Documentation
4
- class NoStepsError < ::Flows::Error
5
- def message
6
- 'No steps defined'
7
- end
8
- end
2
+ class Railway
3
+ # Base class for Railway errors
4
+ class Error < StandardError; end
9
5
 
10
- class NoStepImplementationError < ::Flows::Error
11
- def initialize(step_name)
12
- @step_name = step_name
6
+ # Raised when initializing Railway with no steps
7
+ class NoStepsError < Error
8
+ def initialize(klass)
9
+ @klass = klass
13
10
  end
14
11
 
15
12
  def message
16
- "Missing step definition: #{@step_name}"
13
+ "No steps defined for #{@klass}"
17
14
  end
18
15
  end
19
- # rubocop:enable Style/Documentation
20
16
  end
21
17
  end
@@ -0,0 +1,24 @@
1
+ module Flows
2
+ class Railway
3
+ NODE_PREPROCESSOR = ->(input, _, _) { [[], input.unwrap] }
4
+
5
+ NODE_POSTPROCESSOR = lambda do |output, context, meta|
6
+ context[:last_step] = meta[:name]
7
+
8
+ output
9
+ end
10
+
11
+ # @api private
12
+ Step = Struct.new(:name, :lambda, :next_step, keyword_init: true) do
13
+ def to_node(method_source)
14
+ Flows::Flow::Node.new(
15
+ body: lambda || method_source.method(name),
16
+ router: Flows::Flow::Router::Simple.new(next_step || :end, :end),
17
+ meta: { name: name },
18
+ preprocessor: NODE_PREPROCESSOR,
19
+ postprocessor: NODE_POSTPROCESSOR
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,38 @@
1
+ module Flows
2
+ class Railway
3
+ # @api private
4
+ class StepList
5
+ def initialize
6
+ @list = []
7
+ end
8
+
9
+ def initialize_dup(_other)
10
+ @list = @list.map(&:dup)
11
+ end
12
+
13
+ def add(name:, lambda: nil)
14
+ step = Step.new(name: name, lambda: lambda)
15
+ last_step = @list.last
16
+
17
+ last_step.next_step = name if last_step
18
+
19
+ @list << step
20
+
21
+ self
22
+ end
23
+
24
+ def first_step_name
25
+ @list.first.name
26
+ end
27
+
28
+ # `:reek:FeatureEnvy` is false positive here.
29
+ def to_node_map(method_source)
30
+ @list.map { |step| [step.name, step.to_node(method_source)] }.to_h
31
+ end
32
+
33
+ def empty?
34
+ @list.empty?
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,11 +1,197 @@
1
1
  module Flows
2
- # Result object with context
2
+ # @abstract
3
+ #
4
+ # Result Object is a way of presenting the result of a calculation. The result may be successful or failed.
5
+ #
6
+ # For example, if you calculate expression `a / b`:
7
+ #
8
+ # * for `a = 6` and `b = 2` result will be successful with data `3`.
9
+ # * for `a = 6` and `b = 0` result will be failed with data, for example, `"Cannot divide by zero"`.
10
+ #
11
+ # Examples of such approach may be found in other libraries and languages:
12
+ #
13
+ # * [Either Monad](https://hackage.haskell.org/package/category-extras-0.52.0/docs/Control-Monad-Either.html)
14
+ # in Haskell.
15
+ # * [Result Type](https://doc.rust-lang.org/std/result/enum.Result.html) in Rust.
16
+ # * [Faraday gem](https://www.rubydoc.info/gems/faraday/Faraday/Response) has `Faraday::Response` object
17
+ # which contains data and status.
18
+ # * [dry-rb Result Monad](https://dry-rb.org/gems/dry-monads/1.3/result/) has `Dry::Monads::Result`.
19
+ #
20
+ # So, why do you need Result Object?
21
+ # Why not just return `nil` on a failure or raise an error (like in the standard library)?
22
+ # Here are several reasons:
23
+ #
24
+ # * Raising errors and exceptions is a [bad way](https://martinfowler.com/articles/replaceThrowWithNotification.html)
25
+ # of handling errors.
26
+ # Moreover, it is slow and looks like `goto`.
27
+ # However, it is still a good way to abort execution on an unexpected error.
28
+ # * Returning `nil` does not work when you have to deal with different types of errors or
29
+ # an error has some data payload.
30
+ # * Using specific Result Objects (like `Faraday::Response`) brings inconsistency -
31
+ # you have to learn how to deal with each new type of Result.
32
+ #
33
+ # That's why `Flows` should have Result Object implementation.
34
+ # If any executable Flows entity will return Result Object with the same API -
35
+ # composing your app components becomes trivial.
36
+ # Result Objects should also be as fast and lightweight as possible.
37
+ #
38
+ # Flows' implementation is inspired mainly by [Rust Result Type](https://doc.rust-lang.org/std/result/enum.Result.html)
39
+ # and focused on following features:
40
+ #
41
+ # * Use idiomatic Ruby: no methods named with first capital letter (`Name(1, 2)`), etc.
42
+ # * Use `case` and `===` (case equality) for matching results and writing routing logic.
43
+ # * Provide helpers for convenient creation and matching of Result Objects ({Helpers}).
44
+ # * Result Object may be successful ({Ok}) or failure ({Err}).
45
+ # * Result Object has an {#status} (some symbol: `:saved`, `:zero_division_error`).
46
+ # * Status usage is optional. Default statuses for successful and failure results are `:ok` and `:err`.
47
+ # * Result may have metadata ({#meta}).
48
+ # Metadata is something unrelated to your business logic
49
+ # (execution time, for example, or some info about who created this result).
50
+ # This data must not be used in business logic, it's for a library code.
51
+ # * Different accessors for successful and failure results -
52
+ # prevents treating failure results as successful and vice versa.
53
+ #
54
+ # ## General Recommendations
55
+ #
56
+ # Let's assume that you have some code returning Result Object.
57
+ #
58
+ # * if an error happened and may be handled somehow - return failure result.
59
+ # * if an error happened and cannot be handled - raise exception to abort execution.
60
+ # * if you don't handle any errors for now - don't check result type and
61
+ # use {#unwrap} to access data. It will raise exception when called on a failure result.
62
+ #
63
+ # @example Creating Result Objects
64
+ # # Successful result with data {a: 1}
65
+ # x = Flows::Result::Ok.new(a: 1)
66
+ #
67
+ # # Failure result with data {msg: 'error'}
68
+ # x = Flows::Result::Err.new(msg: 'error')
69
+ #
70
+ # # Successful result with data {a: 1} and status `:done`
71
+ # x = Flows::Result::Ok.new({ a: 1 }, status: :done)
72
+ #
73
+ # # Failure result with data {msg: 'error'} and status `:http_error`
74
+ # x = Flows::Result::Err.new({ msg: 'error' }, status: :http_error)
75
+ #
76
+ # # Successful result with data {a: 1} and metadata { time: 123 }
77
+ # x = Flows::Result::Ok.new({ a: 1 }, meta: { time: 123 })
78
+ #
79
+ # # Failure result with data {msg: 'error'} and metadata { time: 123 }
80
+ # x = Flows::Result::Err.new({ msg: 'error' }, meta: { time: 123 })
81
+ #
82
+ # @example Create Result Objects using helpers
83
+ # class Demo
84
+ # # You cannot provide metadata using helpers and it's ok:
85
+ # # you shouldn't populate metadata in your business code.
86
+ # # Metadata is designed to use in library code and
87
+ # # when you have to provide some metadata from your library -
88
+ # # just use `.new` instead of helpers.
89
+ # include Flows::Result::Helpers
90
+ #
91
+ # def demo
92
+ # # Successful result with data {a: 1}
93
+ # x = ok(a: 1)
94
+ #
95
+ # # Failure result with data {msg: 'error'}
96
+ # x = err(msg: 'error')
97
+ #
98
+ # # Successful result with data {a: 1} and status `:done`
99
+ # x = ok(:done, a: 1)
100
+ #
101
+ # # Failure result with data {msg: 'error'} and status `:http_error`
102
+ # x = err(:http_error, msg: 'error')
103
+ # end
104
+ # end
105
+ #
106
+ # @example Inspecting Result Objects
107
+ # # Behaviour of any result object:
108
+ # result.status # returns status, example: `:ok`
109
+ # result.meta # returns metadata, example: `{}`
110
+ #
111
+ # # Behaviour specific to successful results:
112
+ # result.ok? # true
113
+ # result.err? # false
114
+ # result.unwrap # returns result data
115
+ # result.error # raises exception
116
+ #
117
+ # # Behaviour specific to failure results:
118
+ # result.ok? # false
119
+ # result.err? # true
120
+ # result.unwrap # raises exception
121
+ # result.error # returns result data
122
+ #
123
+ # @example Matching Results with case
124
+ # case result
125
+ # when Flows::Result::Ok then do_job
126
+ # when Flows::Result::Err then give_up
127
+ # end
128
+ #
129
+ # @example Matching Results with case and helpers
130
+ # class Demo
131
+ # include Flows::Result::Helpers
132
+ #
133
+ # def simple_usage
134
+ # case result
135
+ # when match_ok then do_job
136
+ # when match_err then give_up
137
+ # end
138
+ # end
139
+ #
140
+ # def with_status_matching
141
+ # case result
142
+ # when match_ok(:create) then do_create
143
+ # when match_ok(:update) then do_update
144
+ # when match_err(:http_error) then retry
145
+ # when match_err then give_up
146
+ # end
147
+ # end
148
+ # end
149
+ #
150
+ # @!method ok?
151
+ # @abstract
152
+ # @return [Boolean] `true` if result is successful
153
+ # @!method err?
154
+ # @abstract
155
+ # @return [Boolean] `true` if result is failure
156
+ # @!method unwrap
157
+ # @abstract
158
+ # @return [Object] result data
159
+ # @raise [AccessError] if called on failure object
160
+ # @!method error
161
+ # @abstract
162
+ # @return [Object] result data
163
+ # @raise [AccessError] if called on successful object
164
+ #
165
+ # @since 0.4.0
3
166
  class Result
4
- attr_reader :status, :meta
167
+ # @return [Symbol] status of Result Object, default is `:ok` for successful results
168
+ # and `:err` for failure results.
169
+ attr_reader :status
5
170
 
171
+ # @return [Hash] metadata, don't use it to store business data
172
+ attr_reader :meta
173
+
174
+ # Direct creation of this abstract class is forbidden.
175
+ #
176
+ # @raise [StandardError] you will get an error
6
177
  def initialize(**)
7
178
  raise 'Use Flows::Result::Ok or Flows::Result::Err for build result objects'
8
179
  end
180
+
181
+ # Results are equal if have same type and data.
182
+ #
183
+ # Metadata is ignored in comparison.
184
+ #
185
+ # @return [Boolean]
186
+ def ==(other)
187
+ return false if self.class != other.class
188
+
189
+ (status == other.status) && (data == other.send(:data))
190
+ end
191
+
192
+ private
193
+
194
+ attr_accessor :data
9
195
  end
10
196
  end
11
197