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.
- checksums.yaml +4 -4
- data/.github/workflows/{build.yml → test.yml} +5 -10
- data/.gitignore +9 -1
- data/.mdlrc +1 -1
- data/.reek.yml +54 -0
- data/.rubocop.yml +26 -7
- data/.rubocop_todo.yml +27 -0
- data/.ruby-version +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +81 -0
- data/Gemfile +0 -6
- data/README.md +167 -363
- data/Rakefile +35 -1
- data/bin/.rubocop.yml +5 -0
- data/bin/all_the_errors +55 -0
- data/bin/benchmark +73 -105
- data/bin/benchmark_cli/compare.rb +118 -0
- data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
- data/bin/benchmark_cli/compare/base.rb +45 -0
- data/bin/benchmark_cli/compare/command.rb +47 -0
- data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
- data/bin/benchmark_cli/examples.rb +23 -0
- data/bin/benchmark_cli/examples/.rubocop.yml +22 -0
- data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
- data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
- data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
- data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
- data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
- data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
- data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
- data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
- data/bin/benchmark_cli/helpers.rb +12 -0
- data/bin/benchmark_cli/ruby.rb +15 -0
- data/bin/benchmark_cli/ruby/command.rb +38 -0
- data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
- data/bin/benchmark_cli/ruby/self_class.rb +69 -0
- data/bin/benchmark_cli/ruby/structs.rb +90 -0
- data/bin/console +1 -0
- data/bin/docserver +7 -0
- data/bin/errors +138 -0
- data/bin/errors_cli/contract_error_demo.rb +49 -0
- data/bin/errors_cli/di_error_demo.rb +38 -0
- data/bin/errors_cli/flow_error_demo.rb +22 -0
- data/bin/errors_cli/flows_router_error_demo.rb +15 -0
- data/bin/errors_cli/interface_error_demo.rb +17 -0
- data/bin/errors_cli/oc_error_demo.rb +40 -0
- data/bin/errors_cli/railway_error_demo.rb +10 -0
- data/bin/errors_cli/result_error_demo.rb +13 -0
- data/bin/errors_cli/scp_error_demo.rb +17 -0
- data/docs/README.md +3 -187
- data/docs/_sidebar.md +0 -24
- data/docs/index.html +1 -1
- data/flows.gemspec +27 -2
- data/forspell.dict +9 -0
- data/lefthook.yml +9 -0
- data/lib/flows.rb +11 -5
- data/lib/flows/contract.rb +402 -0
- data/lib/flows/contract/array.rb +55 -0
- data/lib/flows/contract/case_eq.rb +43 -0
- data/lib/flows/contract/compose.rb +77 -0
- data/lib/flows/contract/either.rb +53 -0
- data/lib/flows/contract/error.rb +24 -0
- data/lib/flows/contract/hash.rb +75 -0
- data/lib/flows/contract/hash_of.rb +70 -0
- data/lib/flows/contract/helpers.rb +22 -0
- data/lib/flows/contract/predicate.rb +34 -0
- data/lib/flows/contract/transformer.rb +50 -0
- data/lib/flows/contract/tuple.rb +70 -0
- data/lib/flows/flow.rb +96 -7
- data/lib/flows/flow/errors.rb +29 -0
- data/lib/flows/flow/node.rb +132 -0
- data/lib/flows/flow/router.rb +29 -0
- data/lib/flows/flow/router/custom.rb +59 -0
- data/lib/flows/flow/router/errors.rb +11 -0
- data/lib/flows/flow/router/simple.rb +25 -0
- data/lib/flows/plugin.rb +15 -0
- data/lib/flows/plugin/dependency_injector.rb +170 -0
- data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
- data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
- data/lib/flows/plugin/dependency_injector/dependency_list.rb +55 -0
- data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
- data/lib/flows/plugin/implicit_init.rb +45 -0
- data/lib/flows/plugin/interface.rb +84 -0
- data/lib/flows/plugin/output_contract.rb +85 -0
- data/lib/flows/plugin/output_contract/dsl.rb +48 -0
- data/lib/flows/plugin/output_contract/errors.rb +74 -0
- data/lib/flows/plugin/output_contract/wrapper.rb +55 -0
- data/lib/flows/plugin/profiler.rb +114 -0
- data/lib/flows/plugin/profiler/injector.rb +35 -0
- data/lib/flows/plugin/profiler/report.rb +48 -0
- data/lib/flows/plugin/profiler/report/events.rb +43 -0
- data/lib/flows/plugin/profiler/report/flat.rb +41 -0
- data/lib/flows/plugin/profiler/report/flat/method_report.rb +80 -0
- data/lib/flows/plugin/profiler/report/raw.rb +15 -0
- data/lib/flows/plugin/profiler/report/tree.rb +98 -0
- data/lib/flows/plugin/profiler/report/tree/calculated_node.rb +116 -0
- data/lib/flows/plugin/profiler/report/tree/node.rb +34 -0
- data/lib/flows/plugin/profiler/wrapper.rb +53 -0
- data/lib/flows/railway.rb +140 -34
- data/lib/flows/railway/dsl.rb +8 -18
- data/lib/flows/railway/errors.rb +8 -12
- data/lib/flows/railway/step.rb +24 -0
- data/lib/flows/railway/step_list.rb +38 -0
- data/lib/flows/result.rb +188 -2
- data/lib/flows/result/do.rb +158 -16
- data/lib/flows/result/err.rb +12 -6
- data/lib/flows/result/errors.rb +29 -17
- data/lib/flows/result/helpers.rb +25 -3
- data/lib/flows/result/ok.rb +12 -6
- data/lib/flows/shared_context_pipeline.rb +342 -0
- data/lib/flows/shared_context_pipeline/dsl.rb +12 -0
- data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +35 -0
- data/lib/flows/shared_context_pipeline/dsl/tracks.rb +52 -0
- data/lib/flows/shared_context_pipeline/errors.rb +17 -0
- data/lib/flows/shared_context_pipeline/mutation_step.rb +30 -0
- data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
- data/lib/flows/shared_context_pipeline/step.rb +55 -0
- data/lib/flows/shared_context_pipeline/track.rb +54 -0
- data/lib/flows/shared_context_pipeline/track_list.rb +51 -0
- data/lib/flows/shared_context_pipeline/wrap.rb +73 -0
- data/lib/flows/util.rb +17 -0
- data/lib/flows/util/inheritable_singleton_vars.rb +86 -0
- data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +100 -0
- data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +91 -0
- data/lib/flows/util/prepend_to_class.rb +191 -0
- data/lib/flows/version.rb +1 -1
- metadata +253 -38
- data/Gemfile.lock +0 -174
- data/bin/demo +0 -66
- data/bin/examples.rb +0 -195
- data/bin/profile_10steps +0 -106
- data/bin/ruby_benchmarks +0 -26
- data/docs/CNAME +0 -1
- data/docs/contributing/benchmarks_profiling.md +0 -3
- data/docs/contributing/local_development.md +0 -3
- data/docs/flow/direct_usage.md +0 -3
- data/docs/flow/general_idea.md +0 -3
- data/docs/operation/basic_usage.md +0 -1
- data/docs/operation/inject_steps.md +0 -3
- data/docs/operation/lambda_steps.md +0 -3
- data/docs/operation/result_shapes.md +0 -3
- data/docs/operation/routing_tracks.md +0 -3
- data/docs/operation/wrapping_steps.md +0 -3
- data/docs/overview/performance.md +0 -336
- data/docs/railway/basic_usage.md +0 -232
- data/docs/result_objects/basic_usage.md +0 -196
- data/docs/result_objects/do_notation.md +0 -139
- data/lib/flows/node.rb +0 -27
- data/lib/flows/operation.rb +0 -52
- data/lib/flows/operation/builder.rb +0 -130
- data/lib/flows/operation/builder/build_router.rb +0 -37
- data/lib/flows/operation/dsl.rb +0 -93
- data/lib/flows/operation/errors.rb +0 -75
- data/lib/flows/operation/executor.rb +0 -78
- data/lib/flows/railway/builder.rb +0 -68
- data/lib/flows/railway/executor.rb +0 -23
- data/lib/flows/result_router.rb +0 -14
- data/lib/flows/router.rb +0 -22
data/lib/flows/railway.rb
CHANGED
@@ -1,48 +1,154 @@
|
|
1
|
-
require_relative '
|
2
|
-
|
3
|
-
require_relative '
|
4
|
-
require_relative '
|
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
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
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
|
24
|
-
|
25
|
-
|
131
|
+
def initialize
|
132
|
+
klass = self.class
|
133
|
+
steps = klass.steps
|
26
134
|
|
27
|
-
|
135
|
+
raise NoStepsError, klass if steps.empty?
|
28
136
|
|
29
|
-
|
30
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
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
|
data/lib/flows/railway/dsl.rb
CHANGED
@@ -1,27 +1,17 @@
|
|
1
1
|
module Flows
|
2
|
-
|
3
|
-
#
|
2
|
+
class Railway
|
3
|
+
# @api private
|
4
4
|
module DSL
|
5
5
|
attr_reader :steps
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
SingletonVarsSetup = Flows::Util::InheritableSingletonVars::DupStrategy.make_module(
|
8
|
+
'@steps' => StepList.new
|
9
|
+
)
|
9
10
|
|
10
|
-
|
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,
|
21
|
-
|
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
|
data/lib/flows/railway/errors.rb
CHANGED
@@ -1,21 +1,17 @@
|
|
1
1
|
module Flows
|
2
|
-
|
3
|
-
#
|
4
|
-
class
|
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
|
-
|
11
|
-
|
12
|
-
|
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
|
-
"
|
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
|
data/lib/flows/result.rb
CHANGED
@@ -1,11 +1,197 @@
|
|
1
1
|
module Flows
|
2
|
-
#
|
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
|
-
|
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
|
|