flows 0.2.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/{build.yml → test.yml} +5 -10
  3. data/.gitignore +9 -1
  4. data/.mdlrc +1 -1
  5. data/.reek.yml +54 -0
  6. data/.rubocop.yml +26 -7
  7. data/.rubocop_todo.yml +27 -0
  8. data/.ruby-version +1 -1
  9. data/.yardopts +1 -0
  10. data/CHANGELOG.md +81 -0
  11. data/Gemfile +0 -6
  12. data/README.md +167 -363
  13. data/Rakefile +35 -1
  14. data/bin/.rubocop.yml +5 -0
  15. data/bin/all_the_errors +55 -0
  16. data/bin/benchmark +73 -105
  17. data/bin/benchmark_cli/compare.rb +118 -0
  18. data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
  19. data/bin/benchmark_cli/compare/base.rb +45 -0
  20. data/bin/benchmark_cli/compare/command.rb +47 -0
  21. data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
  22. data/bin/benchmark_cli/examples.rb +23 -0
  23. data/bin/benchmark_cli/examples/.rubocop.yml +22 -0
  24. data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
  25. data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
  26. data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
  27. data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
  28. data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
  29. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
  30. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
  31. data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
  32. data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
  33. data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
  34. data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
  35. data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
  36. data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
  37. data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
  38. data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
  39. data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
  40. data/bin/benchmark_cli/helpers.rb +12 -0
  41. data/bin/benchmark_cli/ruby.rb +15 -0
  42. data/bin/benchmark_cli/ruby/command.rb +38 -0
  43. data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
  44. data/bin/benchmark_cli/ruby/self_class.rb +69 -0
  45. data/bin/benchmark_cli/ruby/structs.rb +90 -0
  46. data/bin/console +1 -0
  47. data/bin/docserver +7 -0
  48. data/bin/errors +138 -0
  49. data/bin/errors_cli/contract_error_demo.rb +49 -0
  50. data/bin/errors_cli/di_error_demo.rb +38 -0
  51. data/bin/errors_cli/flow_error_demo.rb +22 -0
  52. data/bin/errors_cli/flows_router_error_demo.rb +15 -0
  53. data/bin/errors_cli/interface_error_demo.rb +17 -0
  54. data/bin/errors_cli/oc_error_demo.rb +40 -0
  55. data/bin/errors_cli/railway_error_demo.rb +10 -0
  56. data/bin/errors_cli/result_error_demo.rb +13 -0
  57. data/bin/errors_cli/scp_error_demo.rb +17 -0
  58. data/docs/README.md +3 -187
  59. data/docs/_sidebar.md +0 -24
  60. data/docs/index.html +1 -1
  61. data/flows.gemspec +27 -2
  62. data/forspell.dict +9 -0
  63. data/lefthook.yml +9 -0
  64. data/lib/flows.rb +11 -5
  65. data/lib/flows/contract.rb +402 -0
  66. data/lib/flows/contract/array.rb +55 -0
  67. data/lib/flows/contract/case_eq.rb +43 -0
  68. data/lib/flows/contract/compose.rb +77 -0
  69. data/lib/flows/contract/either.rb +53 -0
  70. data/lib/flows/contract/error.rb +24 -0
  71. data/lib/flows/contract/hash.rb +75 -0
  72. data/lib/flows/contract/hash_of.rb +70 -0
  73. data/lib/flows/contract/helpers.rb +22 -0
  74. data/lib/flows/contract/predicate.rb +34 -0
  75. data/lib/flows/contract/transformer.rb +50 -0
  76. data/lib/flows/contract/tuple.rb +70 -0
  77. data/lib/flows/flow.rb +96 -7
  78. data/lib/flows/flow/errors.rb +29 -0
  79. data/lib/flows/flow/node.rb +132 -0
  80. data/lib/flows/flow/router.rb +29 -0
  81. data/lib/flows/flow/router/custom.rb +59 -0
  82. data/lib/flows/flow/router/errors.rb +11 -0
  83. data/lib/flows/flow/router/simple.rb +25 -0
  84. data/lib/flows/plugin.rb +15 -0
  85. data/lib/flows/plugin/dependency_injector.rb +170 -0
  86. data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
  87. data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
  88. data/lib/flows/plugin/dependency_injector/dependency_list.rb +55 -0
  89. data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
  90. data/lib/flows/plugin/implicit_init.rb +45 -0
  91. data/lib/flows/plugin/interface.rb +84 -0
  92. data/lib/flows/plugin/output_contract.rb +85 -0
  93. data/lib/flows/plugin/output_contract/dsl.rb +48 -0
  94. data/lib/flows/plugin/output_contract/errors.rb +74 -0
  95. data/lib/flows/plugin/output_contract/wrapper.rb +55 -0
  96. data/lib/flows/plugin/profiler.rb +114 -0
  97. data/lib/flows/plugin/profiler/injector.rb +35 -0
  98. data/lib/flows/plugin/profiler/report.rb +48 -0
  99. data/lib/flows/plugin/profiler/report/events.rb +43 -0
  100. data/lib/flows/plugin/profiler/report/flat.rb +41 -0
  101. data/lib/flows/plugin/profiler/report/flat/method_report.rb +80 -0
  102. data/lib/flows/plugin/profiler/report/raw.rb +15 -0
  103. data/lib/flows/plugin/profiler/report/tree.rb +98 -0
  104. data/lib/flows/plugin/profiler/report/tree/calculated_node.rb +116 -0
  105. data/lib/flows/plugin/profiler/report/tree/node.rb +34 -0
  106. data/lib/flows/plugin/profiler/wrapper.rb +53 -0
  107. data/lib/flows/railway.rb +140 -34
  108. data/lib/flows/railway/dsl.rb +8 -18
  109. data/lib/flows/railway/errors.rb +8 -12
  110. data/lib/flows/railway/step.rb +24 -0
  111. data/lib/flows/railway/step_list.rb +38 -0
  112. data/lib/flows/result.rb +188 -2
  113. data/lib/flows/result/do.rb +158 -16
  114. data/lib/flows/result/err.rb +12 -6
  115. data/lib/flows/result/errors.rb +29 -17
  116. data/lib/flows/result/helpers.rb +25 -3
  117. data/lib/flows/result/ok.rb +12 -6
  118. data/lib/flows/shared_context_pipeline.rb +342 -0
  119. data/lib/flows/shared_context_pipeline/dsl.rb +12 -0
  120. data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +35 -0
  121. data/lib/flows/shared_context_pipeline/dsl/tracks.rb +52 -0
  122. data/lib/flows/shared_context_pipeline/errors.rb +17 -0
  123. data/lib/flows/shared_context_pipeline/mutation_step.rb +30 -0
  124. data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
  125. data/lib/flows/shared_context_pipeline/step.rb +55 -0
  126. data/lib/flows/shared_context_pipeline/track.rb +54 -0
  127. data/lib/flows/shared_context_pipeline/track_list.rb +51 -0
  128. data/lib/flows/shared_context_pipeline/wrap.rb +73 -0
  129. data/lib/flows/util.rb +17 -0
  130. data/lib/flows/util/inheritable_singleton_vars.rb +86 -0
  131. data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +100 -0
  132. data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +91 -0
  133. data/lib/flows/util/prepend_to_class.rb +191 -0
  134. data/lib/flows/version.rb +1 -1
  135. metadata +253 -38
  136. data/Gemfile.lock +0 -174
  137. data/bin/demo +0 -66
  138. data/bin/examples.rb +0 -195
  139. data/bin/profile_10steps +0 -106
  140. data/bin/ruby_benchmarks +0 -26
  141. data/docs/CNAME +0 -1
  142. data/docs/contributing/benchmarks_profiling.md +0 -3
  143. data/docs/contributing/local_development.md +0 -3
  144. data/docs/flow/direct_usage.md +0 -3
  145. data/docs/flow/general_idea.md +0 -3
  146. data/docs/operation/basic_usage.md +0 -1
  147. data/docs/operation/inject_steps.md +0 -3
  148. data/docs/operation/lambda_steps.md +0 -3
  149. data/docs/operation/result_shapes.md +0 -3
  150. data/docs/operation/routing_tracks.md +0 -3
  151. data/docs/operation/wrapping_steps.md +0 -3
  152. data/docs/overview/performance.md +0 -336
  153. data/docs/railway/basic_usage.md +0 -232
  154. data/docs/result_objects/basic_usage.md +0 -196
  155. data/docs/result_objects/do_notation.md +0 -139
  156. data/lib/flows/node.rb +0 -27
  157. data/lib/flows/operation.rb +0 -52
  158. data/lib/flows/operation/builder.rb +0 -130
  159. data/lib/flows/operation/builder/build_router.rb +0 -37
  160. data/lib/flows/operation/dsl.rb +0 -93
  161. data/lib/flows/operation/errors.rb +0 -75
  162. data/lib/flows/operation/executor.rb +0 -78
  163. data/lib/flows/railway/builder.rb +0 -68
  164. data/lib/flows/railway/executor.rb +0 -23
  165. data/lib/flows/result_router.rb +0 -14
  166. data/lib/flows/router.rb +0 -22
@@ -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