flows 0.2.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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