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
@@ -0,0 +1,402 @@
|
|
1
|
+
require_relative 'contract/error'
|
2
|
+
|
3
|
+
require_relative 'contract/case_eq'
|
4
|
+
require_relative 'contract/predicate'
|
5
|
+
|
6
|
+
require_relative 'contract/transformer'
|
7
|
+
require_relative 'contract/compose'
|
8
|
+
require_relative 'contract/either'
|
9
|
+
|
10
|
+
require_relative 'contract/hash'
|
11
|
+
require_relative 'contract/hash_of'
|
12
|
+
require_relative 'contract/array'
|
13
|
+
require_relative 'contract/tuple'
|
14
|
+
|
15
|
+
require_relative 'contract/helpers'
|
16
|
+
|
17
|
+
module Flows
|
18
|
+
# @abstract
|
19
|
+
#
|
20
|
+
# A type contract based on Ruby's case equality.
|
21
|
+
#
|
22
|
+
# ## Motivation
|
23
|
+
#
|
24
|
+
# In ruby we have limited ability to express type contracts.
|
25
|
+
# Because of the dynamic nature of the language we cannot provide type specs or signatures for methods.
|
26
|
+
# We can provide type specs in a form of YARD documentation, but in this way we have no real type checking.
|
27
|
+
# Nothing will stop execution if type contract is violated.
|
28
|
+
#
|
29
|
+
# Flows Contracts are designed to provide runtime type checks for critical places in your code.
|
30
|
+
# Let's review options we have except Flows Contracts and then define what is Flows Contract more strictly.
|
31
|
+
#
|
32
|
+
# Recently in the Ruby community, static/runtime type checking tools started to evolve.
|
33
|
+
# The most advanced solution right now is [Sorbet](https://sorbet.org/).
|
34
|
+
# But Sorbet solves a different problem: it provides static type checking for the whole codebase.
|
35
|
+
# Each method will be checked. Moreover, Sorbet is a tool like bundler or rake,
|
36
|
+
# not just a library.
|
37
|
+
#
|
38
|
+
# In contrast, Flows Contracts are designed to be used in critical places only.
|
39
|
+
# For example to declare input and output contracts for your service objects.
|
40
|
+
# Or to express contracts between application layers
|
41
|
+
# (between Data Access Layer and Business Logic Layer for example).
|
42
|
+
#
|
43
|
+
# As an optional feature Sorbet provides [runtime checks](https://sorbet.org/docs/runtime).
|
44
|
+
# And if you already using Sorbet you may use it to express type contracts also.
|
45
|
+
# The main differences between Sorbet Runtime and Flows Contracts are:
|
46
|
+
#
|
47
|
+
# * Contracts relies on Ruby's case equality and set of helper Contract classes for the most common cases.
|
48
|
+
# Sorbet provides it's own type system and you have to learn it.
|
49
|
+
# * It may be overkill to use Sorbet for expressing contracts only.
|
50
|
+
# In contrast, Flows Contracts are not designed to provide contract for each method in your codebase.
|
51
|
+
# * Sorbet Runtime checks should be a bit faster then Contracts checks (because of transformations).
|
52
|
+
# * The main advantage of Flows Contracts is _transformations_.
|
53
|
+
# It allows you to slightly transform data using Contract
|
54
|
+
# which adds some degree of flexibility to your entities.
|
55
|
+
# See Tranformations section of this documentation for details.
|
56
|
+
#
|
57
|
+
# Let's check what we have for runtime type checking in pure Ruby.
|
58
|
+
# To make some runtime type checks we have at least two ways:
|
59
|
+
#
|
60
|
+
# * methods like `#is_a?`, `#kind_of?` and `#class` can check if subject is an instance of a particular class
|
61
|
+
# * case equality (`===`) in combination with `case` can check different things depends on concrete class.
|
62
|
+
# Check [this article](https://blog.arkency.com/the-equals-equals-equals-case-equality-operator-in-ruby/)
|
63
|
+
# for details.
|
64
|
+
#
|
65
|
+
# As you may see - case equality is already a contract check. We don't need additional checkers to test
|
66
|
+
# if something is a `String` because `String === x` will do the job.
|
67
|
+
# Also lambdas is like predicates with case equality.
|
68
|
+
# Ranges check if subject in a range and regular expressions check for string match.
|
69
|
+
# The problem is that `===` does not provide any error messages.
|
70
|
+
# Second problem - `===` is not an object - it's just a method.
|
71
|
+
# Contract should be an object, it opens more ways of composition.
|
72
|
+
#
|
73
|
+
# _So, Flows Contract is a case equality check wrapped into Contract class instance
|
74
|
+
# with assigned error message and optional transformation logic._
|
75
|
+
#
|
76
|
+
# ## Implementation
|
77
|
+
#
|
78
|
+
# {Contract} is an abstract class which requires {#check!} method
|
79
|
+
# to be implemented. It provides {#===}, {#check}, {#to_proc}, {#transform} and {#transform!} methods for usage in
|
80
|
+
# different scenarios. More details in the methods' documentation.
|
81
|
+
#
|
82
|
+
# {#transform!} must be overriden for types with defined transforming behaviour.
|
83
|
+
# By default no transformation defined - input will be equal to output.
|
84
|
+
# See Transformations and Transformation Laws sections of this documentation for details.
|
85
|
+
#
|
86
|
+
# ## Transformations
|
87
|
+
#
|
88
|
+
# Contract can be used in two ways:
|
89
|
+
#
|
90
|
+
# * to check if data matches a contract ({#check}, {#check!}, {#===}, {#to_proc})
|
91
|
+
# * to check & slightly transform data ({#transform}, {#transform!})
|
92
|
+
#
|
93
|
+
# Transformation is a way to slightly adjust input value before usage.
|
94
|
+
# Good example is when your method accepts both String and Symbol as a name for something,
|
95
|
+
# but internally name should always be a Symbol.
|
96
|
+
# So, contract for this case can be expressed in the following way:
|
97
|
+
#
|
98
|
+
# > Accept either String or Symbol, convert valid value to Symbol
|
99
|
+
#
|
100
|
+
# In this way we still can use both String and Symbol instances as argument,
|
101
|
+
# but in the method's implementation we can be sure that we always get Symbol.
|
102
|
+
#
|
103
|
+
# In the situation when you have to transform one or two arguments
|
104
|
+
# it's easier to merely rely on Ruby's methods like `#to_sym`, `#to_s`, etc.
|
105
|
+
# But in the cases when we talking about 3-6 arguments or nested arguments -
|
106
|
+
# contracts will be more convenient way to express transformations.
|
107
|
+
#
|
108
|
+
# ## Transformation Laws
|
109
|
+
#
|
110
|
+
# When you writing transformations for your contract you MUST implement it
|
111
|
+
# with respect to the following laws:
|
112
|
+
#
|
113
|
+
# # let `c` be an any contract
|
114
|
+
# # let `x` be an any value valid for `c`
|
115
|
+
# # the following statements MUST be true
|
116
|
+
#
|
117
|
+
# # 1. transformed value MUST match the contract:
|
118
|
+
# c.check!(c.transform!(x)) == true
|
119
|
+
#
|
120
|
+
# # 2. tranformation of transformed value MUST has no effect:
|
121
|
+
# c.transform(x) == c.transform(c.transform(x))
|
122
|
+
#
|
123
|
+
# If you violate these laws - you'll get undefined behaviour of contracts.
|
124
|
+
#
|
125
|
+
# The meaning of these laws can be explained through [Equivalence Relation](https://en.wikipedia.org/wiki/Equivalence_relation).
|
126
|
+
# Let's use the following contract as example:
|
127
|
+
#
|
128
|
+
# > Accepts natural numbers except zero in form of String or Integer, transforms to Integer
|
129
|
+
#
|
130
|
+
# We can define a type using a set of all possible type values. For our contract such set can be
|
131
|
+
# described like `[1, '1', 2, '2', ...]`.
|
132
|
+
#
|
133
|
+
# First law says that transformation result must not leave a type.
|
134
|
+
# In other words: transformation is a function from contract type to contract type.
|
135
|
+
#
|
136
|
+
# Second law does two things:
|
137
|
+
#
|
138
|
+
# * split values of type into [equivalence classes](https://en.wikipedia.org/wiki/Equivalence_class)
|
139
|
+
# * for each equivalence class defines one and only one value which should be a transform result for
|
140
|
+
# any value inside the equivalent class. You may call it a tranformation [fixpoint](https://en.wikipedia.org/wiki/Fixed_point_(mathematics)).
|
141
|
+
#
|
142
|
+
# In our example partition will look like this: `[[1, '1'], [2, '2'], ...]`.
|
143
|
+
# Each equivalence class consists of Integer and String form of the same natural number.
|
144
|
+
# And Integer form is a fixpoint.
|
145
|
+
#
|
146
|
+
# Let's review another example:
|
147
|
+
#
|
148
|
+
# > Accepts String, transform is `String#strip`
|
149
|
+
#
|
150
|
+
# In this example each equivalent class is a set of stripped string and all the possible non-stripped variations.
|
151
|
+
# Fixpoint is a stripped string.
|
152
|
+
#
|
153
|
+
# You may think about transformations as transformers (form cinema and animation).
|
154
|
+
# When transformer transforms - it's still the same guy, but in different form (first law).
|
155
|
+
# And fixpoint is transformer main form. We remember Megatron mostly as robot, not as truck. (second law)
|
156
|
+
#
|
157
|
+
# If you find contract transformation too complex abstraction - you can merely not use it.
|
158
|
+
# Flows Contracts without transforms become just type contracts.
|
159
|
+
#
|
160
|
+
# **You MUST be extra careful with transformations and {Compose}.
|
161
|
+
# You cannot just compose any set of types and get a correct result.
|
162
|
+
# See {Compose} documentation for details**
|
163
|
+
#
|
164
|
+
# ## Low-level contracts
|
165
|
+
#
|
166
|
+
# Flows provides some low-level contract classes.
|
167
|
+
# In almost all the cases you don't need to implement your own Contract class
|
168
|
+
# and you only need to compose your contract from this helper classes.
|
169
|
+
#
|
170
|
+
# Wrappers for Ruby objects:
|
171
|
+
#
|
172
|
+
# * {CaseEq} - to wrap Ruby's case equality with error message.
|
173
|
+
# Automatically applied if you pass some Ruby object instead of
|
174
|
+
# {Contract} to some contract initializer.
|
175
|
+
# Please preserve such behaviour in custom contracts.
|
176
|
+
# * {Predicate} - to wrap lambda-check with error message
|
177
|
+
#
|
178
|
+
# Composition and modification of contracts:
|
179
|
+
#
|
180
|
+
# * {Transformer} - to wrap existing contract with some transformation
|
181
|
+
# * {Compose} - to merge two or more contracts
|
182
|
+
# * {Either} - to make "or"-contract from two or more provided contracts. (String or Symbol, for example)
|
183
|
+
#
|
184
|
+
# Contracts for common Ruby collection types:
|
185
|
+
#
|
186
|
+
# * {Hash} - restrict keys by some contract and values by another contract
|
187
|
+
# * {HashOf} - restrict values under particular keys by particular contracts
|
188
|
+
# * {Array} - restrict array elements with some contract
|
189
|
+
# * {Tuple} - restrict fixed-size array elements with contracts
|
190
|
+
#
|
191
|
+
# Using these classes as is can be too verbose and ugly when building complex contracts.
|
192
|
+
# To address this issue Contract class has singleton methods as shortcuts and {.make} class method as DSL:
|
193
|
+
#
|
194
|
+
# # Accepts any string, transforms into stripped variant
|
195
|
+
# strip_str = Flows::Contract.transformer(String, &:strip)
|
196
|
+
#
|
197
|
+
# strip_str === 111
|
198
|
+
# # => false
|
199
|
+
#
|
200
|
+
# strip_str.transform!(' AAA ')
|
201
|
+
# # => 'AAA'
|
202
|
+
#
|
203
|
+
# # Accepts positive integers
|
204
|
+
# pos_int = Flows::Contract.compose(
|
205
|
+
# Integer,
|
206
|
+
# Flows::Contract.predicate('must be positive', &:positive?)
|
207
|
+
# )
|
208
|
+
#
|
209
|
+
# pos_int === 10
|
210
|
+
# # => true
|
211
|
+
#
|
212
|
+
# pos_int === -10
|
213
|
+
# # => false
|
214
|
+
#
|
215
|
+
# # Accepts numbers in String format
|
216
|
+
# str_num = Flows::Contract.make do
|
217
|
+
# compose(
|
218
|
+
# String,
|
219
|
+
# case_eq(/\A\d+\z/, 'must be a number')
|
220
|
+
# )
|
221
|
+
# end
|
222
|
+
#
|
223
|
+
# # Accepts integer or number as string, transforms to integer
|
224
|
+
# pos_int_from_str = transformer(either(Integer, str_num), &:to_i)
|
225
|
+
#
|
226
|
+
# pos_int_from_str === 10
|
227
|
+
# # => true
|
228
|
+
#
|
229
|
+
# pos_int_from_str === '-10'
|
230
|
+
# # => false
|
231
|
+
#
|
232
|
+
# pos_int_from_str.transform!('10')
|
233
|
+
# # => 10
|
234
|
+
#
|
235
|
+
# pos_int_from_str.transform!(10)
|
236
|
+
# # => 10
|
237
|
+
#
|
238
|
+
# # Example of a complex contract
|
239
|
+
# user_contract = Flows::Contract.make do
|
240
|
+
# hash_of(
|
241
|
+
# name: strip_str,
|
242
|
+
# email: strip_str,
|
243
|
+
# password_hash: String,
|
244
|
+
# age: pos_int_from_str,
|
245
|
+
# addresses: array(hash_of(
|
246
|
+
# country: strip_str,
|
247
|
+
# street: strip_str
|
248
|
+
# ))
|
249
|
+
# )
|
250
|
+
# end
|
251
|
+
#
|
252
|
+
# result = user_contract.transform!(
|
253
|
+
# name: ' Roman ',
|
254
|
+
# email: 'bla@blabla.com',
|
255
|
+
# password_hash: '01234567890ABCDEF',
|
256
|
+
# age: '10',
|
257
|
+
# addresses: [],
|
258
|
+
# blabla: 'blablabla' # extra field will be removed by HashOf#transform
|
259
|
+
# )
|
260
|
+
#
|
261
|
+
# result == {
|
262
|
+
# name: 'Roman',
|
263
|
+
# email: 'bla@blabla.com',
|
264
|
+
# password_hash: '01234567890ABCDEF',
|
265
|
+
# age: 10,
|
266
|
+
# addresses: []
|
267
|
+
# }
|
268
|
+
#
|
269
|
+
# All the shortcuts (without {.make}) are available as a separate module: {Helpers}.
|
270
|
+
#
|
271
|
+
# It's up to lead developer how to integrate contracts into app.
|
272
|
+
# You may put contract into constant and use it in the first line of your method.
|
273
|
+
# Or you can write some DSL.
|
274
|
+
# But you should avoid constructing static contracts at runtime -
|
275
|
+
# it's better to instantiate them during loading time (by putting it into constant, for example).
|
276
|
+
#
|
277
|
+
# {Flows::Plugin::OutputContract} in combination with {SharedContextPipeline} will add DSL for contracts.
|
278
|
+
# So, you don't need to invent anything to use output contracts with shared context pipelines.
|
279
|
+
#
|
280
|
+
# ## Private helper methods
|
281
|
+
#
|
282
|
+
# Some private utility methods are defined to simplify new contract implementations:
|
283
|
+
#
|
284
|
+
# `to_contract(value) => Flows::Contract` - if value is a Contract does nothing.
|
285
|
+
# Otherwise wraps value with {CaseEq}. Useful in initializers.
|
286
|
+
#
|
287
|
+
# `merge_nested_errors(description, nested_error) => String` - to make an accurate
|
288
|
+
# multiline error messages with indentation.
|
289
|
+
#
|
290
|
+
# @!method check!( other )
|
291
|
+
# @abstract
|
292
|
+
# Checks for type match.
|
293
|
+
# @return [true] `true` if check succesful
|
294
|
+
# @raise [Flows::Contract::Error] if check failed
|
295
|
+
class Contract
|
296
|
+
# Case equality check.
|
297
|
+
#
|
298
|
+
# Based on {#check!}
|
299
|
+
#
|
300
|
+
# @example Contracts and Ruby's case
|
301
|
+
#
|
302
|
+
# case value
|
303
|
+
# when contract1 then blablabla
|
304
|
+
# when contract2 then blablabla2
|
305
|
+
# end
|
306
|
+
#
|
307
|
+
# @return [Boolean] check result
|
308
|
+
def ===(other)
|
309
|
+
check!(other)
|
310
|
+
true
|
311
|
+
rescue Flows::Contract::Error
|
312
|
+
false
|
313
|
+
end
|
314
|
+
|
315
|
+
# Checks `other` for type match.
|
316
|
+
#
|
317
|
+
# Based on {#check!}.
|
318
|
+
#
|
319
|
+
# @param other [Object] object to check
|
320
|
+
# @return [Flows::Result::Ok<true>] if check successful
|
321
|
+
# @return [Flows::Result::Err<String>] if check failed
|
322
|
+
def check(other)
|
323
|
+
check!(other)
|
324
|
+
Result::Ok.new(true)
|
325
|
+
rescue ::Flows::Contract::Error => err
|
326
|
+
Result::Err.new(err.value_error)
|
327
|
+
end
|
328
|
+
|
329
|
+
# Check and transform value.
|
330
|
+
#
|
331
|
+
# Override this method to implement type transform behaviour.
|
332
|
+
#
|
333
|
+
# If contract is built from other contracts -
|
334
|
+
# all internal contracts must be called via {#transform}.
|
335
|
+
#
|
336
|
+
# You must obey Transformation Laws (see {Contract} class documentation).
|
337
|
+
#
|
338
|
+
# @return [Object] successful result with value after transformation
|
339
|
+
# @raise [Flows::Contract::Error] if check failed
|
340
|
+
def transform!(other)
|
341
|
+
check!(other)
|
342
|
+
other
|
343
|
+
end
|
344
|
+
|
345
|
+
# Check and transform value.
|
346
|
+
#
|
347
|
+
# Based on {#transform!}.
|
348
|
+
#
|
349
|
+
# @return [Flows::Result::Ok<Object>] successful result with value after type transform
|
350
|
+
# @return [Flows::Result::Err<String>] failure result with error message
|
351
|
+
def transform(other)
|
352
|
+
Result::Ok.new(transform!(other))
|
353
|
+
rescue ::Flows::Contract::Error => err
|
354
|
+
Result::Err.new(err.value_error)
|
355
|
+
end
|
356
|
+
|
357
|
+
# Allows to use contract as proc.
|
358
|
+
#
|
359
|
+
# Based on {#===}.
|
360
|
+
#
|
361
|
+
# @example Check all elements in an array
|
362
|
+
# pos_num = Flows::Contract::Predicate.new 'must be positive' do |x|
|
363
|
+
# x > 0
|
364
|
+
# end
|
365
|
+
#
|
366
|
+
# [1, 2, 3].all?(&pos_num)
|
367
|
+
# # => true
|
368
|
+
def to_proc
|
369
|
+
proc do |obj|
|
370
|
+
self === obj # rubocop:disable Style/CaseEquality
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
class << self
|
375
|
+
include Helpers
|
376
|
+
|
377
|
+
# @example
|
378
|
+
# Flows::Contract.make { transformer(either(Symbol, String), &:to_sym) }
|
379
|
+
#
|
380
|
+
# Flows::Contract.make { String }
|
381
|
+
def make(&block)
|
382
|
+
result = instance_exec(&block)
|
383
|
+
|
384
|
+
result.is_a?(Contract) ? result : CaseEq.new(result)
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
private
|
389
|
+
|
390
|
+
# :reek:UtilityFunction
|
391
|
+
def to_contract(value)
|
392
|
+
value.is_a?(::Flows::Contract) ? value : CaseEq.new(value)
|
393
|
+
end
|
394
|
+
|
395
|
+
# :reek:UtilityFunction
|
396
|
+
def merge_nested_errors(description, nested_errors)
|
397
|
+
shifted = nested_errors.split("\n").map { |str| " #{str}" }.join("\n")
|
398
|
+
|
399
|
+
"#{description}\n#{shifted}"
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Flows
|
2
|
+
class Contract
|
3
|
+
# Makes a contract for Array from contract for array's element.
|
4
|
+
#
|
5
|
+
# If underlying contract has transformation -
|
6
|
+
# each array element will be transformed.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# vector = Flows::Contract::Array.new(Numeric)
|
10
|
+
#
|
11
|
+
# vector === 10
|
12
|
+
# # => false
|
13
|
+
#
|
14
|
+
# vector === [10, 20]
|
15
|
+
# # => true
|
16
|
+
class Array < Contract
|
17
|
+
# Stop search for a new type mismatch in elements
|
18
|
+
# if CHECK_LIMIT errors already found.
|
19
|
+
CHECK_LIMIT = 10
|
20
|
+
|
21
|
+
ARRAY_CONTRACT = CaseEq.new(::Array)
|
22
|
+
|
23
|
+
# @param element_contract [Contract, Object] contract for each element. For not-contract values {CaseEq} used.
|
24
|
+
def initialize(element_contract)
|
25
|
+
@contract = to_contract(element_contract)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @see Contract#check!
|
29
|
+
def check!(other)
|
30
|
+
ARRAY_CONTRACT.check!(other)
|
31
|
+
|
32
|
+
raise Error.new(other, report_errors(other)) unless other.all?(&@contract)
|
33
|
+
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
# @see Contract#transform!
|
38
|
+
def transform!(other)
|
39
|
+
check!(other)
|
40
|
+
|
41
|
+
other.map { |elem| @contract.transform!(elem) }
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def report_errors(other)
|
47
|
+
other.reject(&@contract)[0..CHECK_LIMIT].map do |elem|
|
48
|
+
element_error = @contract.check(elem).error
|
49
|
+
|
50
|
+
merge_nested_errors("array element `#{elem.inspect}` is invalid:", element_error)
|
51
|
+
end.join("\n")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|