flows 0.3.0 → 0.4.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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/{build.yml → test.yml} +5 -10
  3. data/.gitignore +1 -0
  4. data/.reek.yml +42 -0
  5. data/.rubocop.yml +20 -7
  6. data/.ruby-version +1 -1
  7. data/.yardopts +1 -0
  8. data/CHANGELOG.md +42 -0
  9. data/Gemfile +0 -6
  10. data/Gemfile.lock +139 -74
  11. data/README.md +158 -364
  12. data/Rakefile +35 -1
  13. data/bin/.rubocop.yml +5 -0
  14. data/bin/all_the_errors +47 -0
  15. data/bin/benchmark +73 -105
  16. data/bin/benchmark_cli/compare.rb +118 -0
  17. data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
  18. data/bin/benchmark_cli/compare/base.rb +45 -0
  19. data/bin/benchmark_cli/compare/command.rb +47 -0
  20. data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
  21. data/bin/benchmark_cli/examples.rb +23 -0
  22. data/bin/benchmark_cli/examples/.rubocop.yml +19 -0
  23. data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
  24. data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
  25. data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
  26. data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
  27. data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
  28. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
  29. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
  30. data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
  31. data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
  32. data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
  33. data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
  34. data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
  35. data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
  36. data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
  37. data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
  38. data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
  39. data/bin/benchmark_cli/helpers.rb +12 -0
  40. data/bin/benchmark_cli/ruby.rb +15 -0
  41. data/bin/benchmark_cli/ruby/command.rb +38 -0
  42. data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
  43. data/bin/benchmark_cli/ruby/self_class.rb +69 -0
  44. data/bin/benchmark_cli/ruby/structs.rb +90 -0
  45. data/bin/console +1 -0
  46. data/bin/docserver +7 -0
  47. data/bin/errors +118 -0
  48. data/bin/errors_cli/contract_error_demo.rb +49 -0
  49. data/bin/errors_cli/di_error_demo.rb +38 -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/README.md +2 -186
  56. data/docs/_sidebar.md +0 -24
  57. data/docs/index.html +1 -1
  58. data/flows.gemspec +25 -2
  59. data/forspell.dict +9 -0
  60. data/lefthook.yml +9 -0
  61. data/lib/flows.rb +11 -5
  62. data/lib/flows/contract.rb +402 -0
  63. data/lib/flows/contract/array.rb +55 -0
  64. data/lib/flows/contract/case_eq.rb +41 -0
  65. data/lib/flows/contract/compose.rb +77 -0
  66. data/lib/flows/contract/either.rb +53 -0
  67. data/lib/flows/contract/error.rb +25 -0
  68. data/lib/flows/contract/hash.rb +75 -0
  69. data/lib/flows/contract/hash_of.rb +70 -0
  70. data/lib/flows/contract/helpers.rb +22 -0
  71. data/lib/flows/contract/predicate.rb +34 -0
  72. data/lib/flows/contract/transformer.rb +50 -0
  73. data/lib/flows/contract/tuple.rb +70 -0
  74. data/lib/flows/flow.rb +75 -7
  75. data/lib/flows/flow/node.rb +131 -0
  76. data/lib/flows/flow/router.rb +25 -0
  77. data/lib/flows/flow/router/custom.rb +54 -0
  78. data/lib/flows/flow/router/errors.rb +11 -0
  79. data/lib/flows/flow/router/simple.rb +20 -0
  80. data/lib/flows/plugin.rb +13 -0
  81. data/lib/flows/plugin/dependency_injector.rb +159 -0
  82. data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
  83. data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
  84. data/lib/flows/plugin/dependency_injector/dependency_list.rb +57 -0
  85. data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
  86. data/lib/flows/plugin/implicit_init.rb +45 -0
  87. data/lib/flows/plugin/output_contract.rb +84 -0
  88. data/lib/flows/plugin/output_contract/dsl.rb +36 -0
  89. data/lib/flows/plugin/output_contract/errors.rb +74 -0
  90. data/lib/flows/plugin/output_contract/wrapper.rb +53 -0
  91. data/lib/flows/railway.rb +140 -37
  92. data/lib/flows/railway/dsl.rb +8 -19
  93. data/lib/flows/railway/errors.rb +8 -12
  94. data/lib/flows/railway/step.rb +24 -0
  95. data/lib/flows/railway/step_list.rb +38 -0
  96. data/lib/flows/result.rb +188 -2
  97. data/lib/flows/result/do.rb +160 -16
  98. data/lib/flows/result/err.rb +12 -6
  99. data/lib/flows/result/errors.rb +29 -17
  100. data/lib/flows/result/helpers.rb +25 -3
  101. data/lib/flows/result/ok.rb +12 -6
  102. data/lib/flows/shared_context_pipeline.rb +216 -0
  103. data/lib/flows/shared_context_pipeline/dsl.rb +63 -0
  104. data/lib/flows/shared_context_pipeline/errors.rb +17 -0
  105. data/lib/flows/shared_context_pipeline/mutation_step.rb +31 -0
  106. data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
  107. data/lib/flows/shared_context_pipeline/step.rb +46 -0
  108. data/lib/flows/shared_context_pipeline/track.rb +67 -0
  109. data/lib/flows/shared_context_pipeline/track_list.rb +46 -0
  110. data/lib/flows/util.rb +17 -0
  111. data/lib/flows/util/inheritable_singleton_vars.rb +79 -0
  112. data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +109 -0
  113. data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +104 -0
  114. data/lib/flows/util/prepend_to_class.rb +145 -0
  115. data/lib/flows/version.rb +1 -1
  116. metadata +233 -37
  117. data/bin/demo +0 -66
  118. data/bin/examples.rb +0 -195
  119. data/bin/profile_10steps +0 -106
  120. data/bin/ruby_benchmarks +0 -26
  121. data/docs/CNAME +0 -1
  122. data/docs/contributing/benchmarks_profiling.md +0 -3
  123. data/docs/contributing/local_development.md +0 -3
  124. data/docs/flow/direct_usage.md +0 -3
  125. data/docs/flow/general_idea.md +0 -3
  126. data/docs/operation/basic_usage.md +0 -1
  127. data/docs/operation/inject_steps.md +0 -3
  128. data/docs/operation/lambda_steps.md +0 -3
  129. data/docs/operation/result_shapes.md +0 -3
  130. data/docs/operation/routing_tracks.md +0 -3
  131. data/docs/operation/wrapping_steps.md +0 -3
  132. data/docs/overview/performance.md +0 -336
  133. data/docs/railway/basic_usage.md +0 -232
  134. data/docs/result_objects/basic_usage.md +0 -196
  135. data/docs/result_objects/do_notation.md +0 -139
  136. data/lib/flows/implicit_build.rb +0 -16
  137. data/lib/flows/node.rb +0 -27
  138. data/lib/flows/operation.rb +0 -55
  139. data/lib/flows/operation/builder.rb +0 -130
  140. data/lib/flows/operation/builder/build_router.rb +0 -37
  141. data/lib/flows/operation/dsl.rb +0 -93
  142. data/lib/flows/operation/errors.rb +0 -75
  143. data/lib/flows/operation/executor.rb +0 -78
  144. data/lib/flows/railway/builder.rb +0 -68
  145. data/lib/flows/railway/executor.rb +0 -23
  146. data/lib/flows/result_router.rb +0 -14
  147. data/lib/flows/router.rb +0 -22
@@ -1,232 +0,0 @@
1
- # Railway :: Basic Usage
2
-
3
- `Flows::Railway` is an implementation of a Railway Programming pattern. You may read about this pattern in the following articles:
4
-
5
- * [Programming on rails: Railway Oriented Programming](http://sandordargo.com/blog/2017/09/27/railway_oriented_programming) // it's not about Ruby on Rails
6
- * [Railway Oriented Programming: A powerful Functional Programming pattern](https://medium.com/@naveenkumarmuguda/railway-oriented-programming-a-powerful-functional-programming-pattern-ab454e467f31)
7
- * [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)
8
-
9
- Let's review a simple task and solve it using `Flows::Railway`: you have to get a user by ID, get all user's blog posts and convert it to an array of HTML-strings. In such situation, we have to implement three parts of our task and compose it into something we can call, for example, from a Rails controller. Also, the first and third steps may fail (user not found, conversion to HTML failed). And if a step failed - we have to return failure info immediately. Let's draw this using a UML activity diagram:
10
-
11
- ```plantuml
12
- @startuml
13
- |Success Path|
14
- start
15
- -> id: Integer;
16
- :fetch_user;
17
- if (success?) then (yes)
18
- -> user: User;
19
- :get_blog_posts;
20
- -> posts: Array<Post>;
21
- :convert_to_html;
22
- if (success?) then (yes)
23
- -> posts_html: Array<String>;
24
- stop
25
- else (no)
26
- |Failure|
27
- -> message: String;
28
- end
29
- endif
30
- else (no)
31
- |Failure|
32
- -> message: String;
33
- end
34
- endif
35
- @enduml
36
- ```
37
-
38
- And implement using `Flows::Railway`:
39
-
40
- ```ruby
41
- class RenderUserBlogPosts
42
- include Flows::Railway
43
-
44
- step :fetch_user
45
- step :get_blog_posts
46
- step :convert_to_html
47
-
48
- def fetch_user(id:)
49
- user = User.find_by_id(id)
50
- user ? ok(user: user) : err(message: "User #{id} not found")
51
- end
52
-
53
- def get_blog_posts(user:)
54
- ok(posts: User.posts)
55
- end
56
-
57
- def convert_to_html(posts:)
58
- posts_html = post.map(&:text).map do |text|
59
- html = convert(text)
60
- return err(message: "cannot convert to html: #{text}")
61
- end
62
-
63
- ok(posts_html: posts_html)
64
- end
65
-
66
- private
67
-
68
- # returns String or nil
69
- def convert(text)
70
- # some implementation here
71
- end
72
- end
73
- ```
74
-
75
- And execute it:
76
-
77
- ```ruby
78
- # User with id = 1 exists and with id = 2 - doesn't
79
-
80
- RenderUserBlogPosts.new.call(id: 1)
81
- # => Flows::Result::Ok.new(posts_html: [...])
82
-
83
- RenderUserBlogPosts.new.call(id: 2)
84
- # => Flows::Result::Err.new(message: 'User 2 not found')
85
- ```
86
-
87
- ## Flows::Railway rules
88
-
89
- * steps execution happens from the first to the last step
90
- * input arguments (`Railway#call(...)`) becomes the input of the first step
91
- * each step should return Result Object (`Flows::Result::Helpers` already included)
92
- * if step returns failed result - execution stops and failed Result Object returned from Railway
93
- * if step returns successful result - result data becomes arguments of the following step
94
- * if the last step returns successful result - it becomes a result of a Railway execution
95
-
96
- ## Defining Steps
97
-
98
- Two ways of step definition exist. First is by using an instance method:
99
-
100
- ```ruby
101
- step :do_something
102
-
103
- def do_something(**arguments)
104
- # some implementation
105
- # Result Object as return value
106
- end
107
- ```
108
-
109
- Second is by using lambda:
110
-
111
- ```ruby
112
- step :do_something, ->(**arguments) { ok(some: 'data') }
113
- ```
114
-
115
- Definition with lambda exists primarily for debugging/testing purposes. I recommend you to use method-based implementations for all your business logic. Also, this is good for consistency, readability, and maintenance. __Think about Railway as about small book: you have a "table of contents" in a form of step definitions and actual "chapters" in the same order in a form of public methods. And your private methods becomes something like "appendix".__
116
-
117
- ## Dependency Injection
118
-
119
- By default, we search for step implementation methods in a class instance. But you may override method source and inject your own:
120
-
121
- ```ruby
122
- class SayOk
123
- include Flows::Railway
124
-
125
- step :do_job
126
- end
127
-
128
- module Loud
129
- extend Flows::Result::Helpers
130
-
131
- def self.do_job
132
- ok(text: 'OOOOKKKK!!!!')
133
- end
134
- end
135
-
136
- module Normal
137
- extend Flows::Result::Helpers
138
-
139
- def self.do_job
140
- ok(text: 'ok')
141
- end
142
- end
143
-
144
- SayOk.new(method_source: Loud).call.unwrap
145
- # => { text: 'OOOOKKKK!!!!' }
146
-
147
- SayOk.new(method_source: Normal).call.unwrap
148
- # => { text: 'ok' }
149
- ```
150
-
151
- When you change your method source original class is no longer used for methods lookup. But what if we want to just override one of the steps? We can:
152
-
153
- ```ruby
154
- class SayOk
155
- include Flows::Railway
156
-
157
- step :do_job
158
-
159
- def do_job
160
- ok(text: 'ok')
161
- end
162
- end
163
-
164
- say_loud = -> { ok(text: 'OOOOKKKK!!!!') } # or anything with implemented #call method
165
-
166
- SayOk.new.call.unwrap
167
- # => { text: 'OOOOKKKK!!!!' }
168
-
169
- SayOk.new(deps: { do_job: say_loud }).call.unwrap
170
- # => { text: 'ok' }
171
- ```
172
-
173
- Moreover, you can mix both approaches. Injecting using `deps:` has higher priority.
174
-
175
- ## Pre-building and Performance
176
-
177
- As mentioned before, railway execution consists of two phases: build (`.new`) and run (`#call`). And the build phase is expensive. You may compare overheads when you build a railway each time:
178
-
179
- ```
180
- $ WITH_RW=1 bin/benchmark
181
-
182
- --------------------------------------------------
183
- - task: A + B, one step implementation
184
- --------------------------------------------------
185
- Warming up --------------------------------------
186
- Flows::Railway (build once)
187
- 30.995k i/100ms
188
- Flows::Railway (build each time)
189
- 11.553k i/100ms
190
- Calculating -------------------------------------
191
- Flows::Railway (build once)
192
- 347.682k (± 2.1%) i/s - 1.767M in 5.083828s
193
- Flows::Railway (build each time)
194
- 122.908k (± 4.2%) i/s - 623.862k in 5.085459s
195
-
196
- Comparison:
197
- Flows::Railway (build once): 347681.6 i/s
198
- Flows::Railway (build each time): 122908.0 i/s - 2.83x slower
199
-
200
-
201
- --------------------------------------------------
202
- - task: ten steps returns successful result
203
- --------------------------------------------------
204
- Warming up --------------------------------------
205
- Flows::Railway (build once)
206
- 6.130k i/100ms
207
- Flows::Railway (build each time)
208
- 2.168k i/100ms
209
- Calculating -------------------------------------
210
- Flows::Railway (build once)
211
- 63.202k (± 1.6%) i/s - 318.760k in 5.044862s
212
- Flows::Railway (build each time)
213
- 21.645k (± 3.6%) i/s - 108.400k in 5.014725s
214
-
215
- Comparison:
216
- Flows::Railway (build once): 63202.5 i/s
217
- Flows::Railway (build each time): 21645.2 i/s - 2.92x slower
218
- ```
219
-
220
- As the benchmark shows your infrastructure code overhead from Flows will be almost three times lower when you build your railways at 'compile' time. I mean something like that:
221
-
222
- ```ruby
223
- class MyClass
224
- MY_RAILWAY = MyRailway.new # this string will be executed on a class loading stage
225
-
226
- def my_method
227
- MY_RAILWAY.call
228
- end
229
- end
230
- ```
231
-
232
- But if you don't care much about performance - build each time will be fast enough. Check out [Performance](overview/performance.md) page to see a bigger picture.
@@ -1,196 +0,0 @@
1
- # Result Object :: Basic Usage
2
-
3
- Result Object is a way of presenting the result of a calculation. The result may be successful or failed.
4
- For example, if you calculate expression `a / b`:
5
-
6
- * for `a = 6` and `b = 2` result will be successful with data `3`.
7
- * for `a = 6` and `b = 0` result will be failed with data, for example, `"Cannot divide by zero"`.
8
-
9
- Examples of such approach may be found in other libraries and languages:
10
-
11
- * [Either Monad](https://hackage.haskell.org/package/category-extras-0.52.0/docs/Control-Monad-Either.html) in Haskell
12
- * [Result Type](https://doc.rust-lang.org/std/result/enum.Result.html) in Rust
13
- * [Faraday gem](https://www.rubydoc.info/gems/faraday/Faraday/Response) has `Faraday::Response` object which contains data and status
14
- * [dry-rb Result Monad](https://dry-rb.org/gems/dry-monads/result/) has `Dry::Monads::Result`
15
-
16
- So, why do you need Result Object? Why not just return `nil` on a failure or raise an error (like in the standard library)? Here are several reasons:
17
-
18
- * raising errors and exceptions isn't a very convenient and explicit way to handle errors. Moreover, it is slow and looks like `goto`. However, it is still a good way to abort execution on an unexpected error.
19
- * returning `nil` does not work when you have to deal with different types of errors or an error has some data payload.
20
- * using specific Result Objects (like `Faraday::Response`) brings inconsistency - you have to learn how to deal with each new type of Result.
21
-
22
- That's why `Flows` should have Result Object implementation. If any executable Flows entity will return Result Object with the same API - composing your app components becomes trivial. Result Objects should also be as fast and lightweight as possible.
23
-
24
- Flows' implementation is inspired mainly by [Rust Result Type](https://doc.rust-lang.org/std/result/enum.Result.html) and focused on following features:
25
-
26
- * use idiomatic Ruby: no methods named with first capital letter (`Name(1, 2)`), etc.
27
- * provide convenient helpers for `case` and `===` (case equality) for matching results and writing routing logic
28
- * provide helpers for convenient creation of Result Objects
29
- * Result Object may be successful (`Ok`) or failure (`Err`)
30
- * Result Object has an status (some symbol: `:saved`, `:zero_division_error`)
31
- * status usage is optional. Default statuses for successful and failure results are `:success` and `:failure`
32
- * result may have metadata. Metadata is something unrelated to your business logic (execution time, for example, or some info about who created this result).
33
- * different accessors for successful and failure results - prevents treating failure results as successful and vice versa.
34
-
35
- ## Class Diagram
36
-
37
- Class UML diagram describing current implementation:
38
-
39
- ```plantuml
40
- @startuml
41
- class Flows::Result<Abstract Class> {
42
- .. Constructor ..
43
- {static} new(Symbol status, Hash data, Hash metadata)
44
- .. Success checks ..
45
- {abstract} bool ok?()
46
- {abstract} bool err?()
47
- .. Result data access ..
48
- Symbol status()
49
- {abstract} Hash unwrap()
50
- {abstract} Hash error()
51
- .. Metadata ..
52
- Hash meta()
53
- }
54
-
55
- class Flows::Result::Ok {
56
- true ok?()
57
- false err?()
58
- ..
59
- Hash unwrap()
60
- [raise exception] error()
61
- }
62
-
63
- class Flows::Result::Err {
64
- false ok?()
65
- true err?()
66
- ..
67
- [raise exception] unwrap()
68
- Hash error()
69
- }
70
-
71
- Flows::Result --> Flows::Result::Ok
72
- Flows::Result --> Flows::Result::Err
73
- @enduml
74
- ```
75
-
76
- ## Creating Results
77
-
78
- Most flexible and verbose way of creating Result Objects is creating via `.new`:
79
-
80
- ```ruby
81
- # Successful result with data {a: 1}
82
- Flows::Result::Ok.new(a: 1)
83
-
84
- # Failure result with data {msg: 'error'}
85
- Flows::Result::Err.new(msg: 'error')
86
-
87
- # Successful result with data {a: 1} and status `:done`
88
- Flows::Result::Ok.new({ a: 1 }, status: :done)
89
-
90
- # Failure result with data {msg: 'error'} and status `:http_error`
91
- Flows::Result::Err.new({ msg: 'error' }, status: :http_error)
92
-
93
- # Successful result with data {a: 1} and metadata `{ time: 123 }`
94
- Flows::Result::Ok.new({ a: 1 }, meta: { time: 123 })
95
-
96
- # Failure result with data {msg: 'error'} and metadata `{ time: 123 }`
97
- Flows::Result::Err.new({ msg: 'error' }, meta: { time: 123 })
98
- ```
99
-
100
- More convenient and short way is to use helpers:
101
-
102
- ```ruby
103
- include Flows::Result::Helpers
104
-
105
- # Successful result with data {a: 1}
106
- ok(a: 1)
107
-
108
- # Failure result with data {msg: 'error'}
109
- err(msg: 'error')
110
-
111
- # Successful result with data {a: 1} and status `:done`
112
- ok(:done, a: 1)
113
-
114
- # Failure result with data {msg: 'error'} and status `:http_error`
115
- err(:http_error, msg: 'error')
116
- ```
117
-
118
- You cannot provide metadata using helpers and it's ok: you shouldn't populate metadata in your business code.
119
- Metadata is designed to use in library code and when you have to provide some metadata from your library - just use `.new` instead of helpers.
120
-
121
- ## Inspecting Results
122
-
123
- Behaviour of any result object:
124
-
125
- ```ruby
126
- result.status # returns status, example: `:success`
127
-
128
- result.meta # returns metadata, example: `{}`
129
- ```
130
-
131
- Behaviour specific to successful results:
132
-
133
- ```ruby
134
- result.ok? # true
135
-
136
- result.err? # false
137
-
138
- result.unwrap # returns result data
139
-
140
- result.error # raises exception
141
- ```
142
-
143
- Behaviour specific to failure results:
144
-
145
- ```ruby
146
- result.ok? # false
147
-
148
- result.err? # true
149
-
150
- result.unwrap # raises exception
151
-
152
- result.error # returns result data
153
- ```
154
-
155
- ## Matching Results
156
-
157
- Basic matching results using `case`:
158
-
159
- ```ruby
160
- case result
161
- when Flows::Result::Ok then do_job
162
- when Flows::Results::Err then give_up
163
- end
164
- ```
165
-
166
- But this is too verbose. For this case helpers has methods for matching. Example above may be rewritten like this:
167
-
168
- ```ruby
169
- include Flows::Result::Helpers
170
-
171
- case result
172
- when match_ok then do_job
173
- when match_err then give_up
174
- end
175
- ```
176
-
177
- Moreover, you may specify status when using helper matchers:
178
-
179
- ```ruby
180
- include Flows::Result::Helpers
181
-
182
- case result
183
- when match_ok(:create) then do_create
184
- when match_ok(:update) then do_update
185
- when match_err(:http_error) then retry
186
- when match_err then give_up
187
- end
188
- ```
189
-
190
- ## General Recommendations
191
-
192
- Let's assume that you have some code returning Result Object.
193
-
194
- * if error happened and may be handled somehow - return failure result
195
- * if error happened and cannot be handled - raise exception to abort execution
196
- * if you don't handle any errors for now - don't check result type and use `#unwrap` to access data. It will raise exception when called on a failure result.
@@ -1,139 +0,0 @@
1
- # Result Object :: Do Notation
2
-
3
- This functionality aims to simplify common control flow pattern: when you have to stop execution on a first failure and return this failure.
4
- Do Notation inspired by [Do Notation in dry-rb](https://dry-rb.org/gems/dry-monads/do-notation/) and [Haskell do keyword](https://wiki.haskell.org/Keywords#do).
5
-
6
- Sometimes you have to write something like this:
7
-
8
- ```ruby
9
- class Something
10
- include Flows::Result::Helpers
11
-
12
- def do_job
13
- user_result = fetch_user
14
- return user_result if user_result.err?
15
-
16
- data_result = fetch_data
17
- return data_result if data_result.err?
18
-
19
- calculation_result = calculation(user_result.unwrap[:user], data_result.unwrap)
20
- return calculation_result if user_result.err?
21
-
22
- ok(data: calculation_result.unwrap[:some_field])
23
- end
24
-
25
- private
26
-
27
- def fetch_user
28
- # returns Ok or Err
29
- end
30
-
31
- def fetch_data
32
- # returns Ok or Err
33
- end
34
-
35
- def calculation(_user, _data)
36
- # returns Ok or Err
37
- end
38
- end
39
- ```
40
-
41
- The main idea of the code above is to stop method execution and return failed Result Object if one of the sub-operations is failed. At the moment of failure.
42
-
43
- By using Do Notation feature you may rewrite it like this:
44
-
45
- ```ruby
46
- class SomethingWithDoNotation
47
- include Flows::Result::Helpers
48
- include Flows::Result::Do # enable Do Notation
49
-
50
- do_for(:do_job) # changes behaviour of `yield` in this method
51
- def do_job
52
- user, = yield :user, fetch_user # yield here returns array of one element
53
- data = yield fetch_data # yield here returns a Hash
54
-
55
- ok(data: yield(:some_field, calculation(user, data))[0])
56
- end
57
-
58
- # private method definitions
59
- end
60
- ```
61
-
62
- or like this:
63
-
64
- ```ruby
65
- do_for(:do_job)
66
- def do_job
67
- user = yield(fetch_user)[:user] # yield here and below returns a Hash
68
- data = yield fetch_data
69
-
70
- ok(data: yield(calculation(user, data))[:some_field])
71
- end
72
- ```
73
-
74
- `do_for(:do_job)` makes some simple magic here and allows you to use `yield` inside `do_job` in a non standard way:
75
- to unpack results or instantly leave a method if a failed result provided.
76
-
77
- ## How to use it
78
-
79
- First of all, you have to include `Flows::Result::Do` mixin into your class or module. It adds `do_for` class method.
80
- `do_for` accepts method name as an argument and changes behaviour of `yield` inside this method. By the way, when you are using
81
- `do_for` you cannot pass a block to modified method anymore.
82
-
83
- Then `do_for` method should be used to enable Do Notation for certain methods.
84
-
85
- ```ruby
86
- class MyClass
87
- include Flows::Result::Do
88
-
89
- do_for(:my_method_1)
90
- def my_method_1
91
- # some code
92
- end
93
-
94
- do_for(:my_method_2)
95
- def my_method_2
96
- # some code
97
- end
98
- end
99
- ```
100
-
101
- `yield` in such methods starts working by following rules:
102
-
103
- ```ruby
104
- ok_result = Flows::Result::Ok.new(a: 1, b: 2)
105
- err_result = Flows::Result::Err.new(x: 1, y: 2)
106
-
107
- # following three lines are equivalent
108
- yield(ok_result)
109
- ok_result.unwrap
110
- { a: 1, b: 2 }
111
-
112
- # following three lines are equivalent
113
- yield(:a, :b, ok_result)
114
- ok_result.unwrap.values_at(:a, :b)
115
- [1, 2]
116
-
117
- # following two lines are equivalent
118
- yield(err_result)
119
- return err_result
120
-
121
- # following two lines are equivalent
122
- yield(:x, :y, err_result)
123
- return err_result
124
- ```
125
-
126
- As you may see, `yield` has two forms of usage:
127
-
128
- * `yield(result_value)` - returns unwrapped data Hash for successful results or,
129
- in case of failed result, stops method execution and returns failed `result_value` as a method result.
130
- * `yield(*keys, result_value)` - returns unwrapped data under provided keys as Array for successful results or,
131
- in case of failed result, stops method execution and returns failed `result_value` as a method result.
132
-
133
- ## How it works
134
-
135
- Under the hood `Flows::Result::Do` creates a module and prepends it to your class or module.
136
- Invoking of `do_for(:method_name)` adds special wrapper method to the prepended module. So, when you perform call to
137
- `YourClassOrModule#method_name` - you execute wrapper in the prepended module.
138
-
139
- Check out source code for implementation details.