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,43 @@
1
+ module Flows
2
+ class Contract
3
+ # Makes a contract from provided object's case equality check.
4
+ #
5
+ # @example String contract
6
+ # string_check = Flows::Contract::CaseEq.new(String)
7
+ #
8
+ # string_check.check(111)
9
+ # # => Flows::Result::Err.new('must match `String`')
10
+ #
11
+ # string_check === 'sdfdsfsd'
12
+ # # => true
13
+ #
14
+ # @example Integer contract with custom error message
15
+ # int_check = Flows::Contract::CaseEq.new(Integer, 'must be an integer')
16
+ #
17
+ # int_check.check('111')
18
+ # # => Flows::Result::Err.new('must be an integer')
19
+ #
20
+ # string_check === 'sdfdsfsd'
21
+ # # => true
22
+ class CaseEq < Contract
23
+ # @param object [#===] object with case equality
24
+ # @param error_message [String] you may override default error message
25
+ def initialize(object, error_message = nil)
26
+ @object = object
27
+ @error_message = error_message
28
+ end
29
+
30
+ # @see Contract#check!
31
+ def check!(other)
32
+ unless @object === other
33
+ value_error =
34
+ @error_message ||
35
+ "must match `#{@object.inspect}`, but has class `#{other.class.inspect}` and value `#{other.inspect}`"
36
+ raise Error.new(other, value_error)
37
+ end
38
+
39
+ true
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,77 @@
1
+ module Flows
2
+ class Contract
3
+ # Allows to combine two or more contracts.
4
+ #
5
+ # From type system perspective - this composition is intersection of types.
6
+ # It means that value passes contract if it passes each particular contract in a composition.
7
+ #
8
+ # ## Composition and Transform Laws
9
+ #
10
+ # _Golden rule:_ don't use contracts with transformations in composition if you can.
11
+ # In the most cases you can compose contracts without transformations
12
+ # and apply one transformation to composite contract.
13
+ #
14
+ # Composition of contracts' transformations MUST obey Transform Laws (see {Contract} documentation for details).
15
+ # To achieve this each particular transform MUST obey following additional laws:
16
+ #
17
+ # # let `c` be a contract composition
18
+ #
19
+ # # 1. each transform should not leave composite type
20
+ # #
21
+ # # for any `x` valid for composite type
22
+ # c.check!(x) == true
23
+ # # and for any contract `c_i` from composition:
24
+ # c.check!(c_i.transform!(x)) == true
25
+ #
26
+ # # 2. tranforms can be applied in any order
27
+ # #
28
+ # # for any `x` valid for composite type
29
+ # c.check!(x) == true
30
+ # # for any two contracts `c_i` and `c_j` from composition:
31
+ # c_i(c_j(x)) == c_j(c_i(x))
32
+ #
33
+ # Why do we need the first law?
34
+ # To prevent situations when original value matches composite type,
35
+ # but transformed value doesn't. Example:
36
+ #
37
+ # Flows::Contract.make do
38
+ # compose(
39
+ # transform(either(String, Symbol), &:to_sym),
40
+ # String
41
+ # )
42
+ # end
43
+ #
44
+ # Second laws makes composition of transforms to obey 2nd transform law.
45
+ # Example of correct composable transforms:
46
+ #
47
+ # Flows::Contract.make do
48
+ # compose(
49
+ # transform(String, &:strip),
50
+ # transform(String, &:trim)
51
+ # )
52
+ # end
53
+ #
54
+ # Formal proof is based on [this theorem proof](https://math.stackexchange.com/questions/600978/equivalence-relation-composition-problem).
55
+ class Compose < Contract
56
+ # @param contracts [Array<Contract, Object>] contract list. Non-contract elements will be wrapped with {CaseEq}.
57
+ def initialize(*contracts)
58
+ raise 'Contract list must not be empty' if contracts.length.zero?
59
+
60
+ @contracts = contracts.map(&method(:to_contract))
61
+ end
62
+
63
+ # @see Contract#check!
64
+ def check!(other)
65
+ @contracts.each { |con| con.check!(other) }
66
+ true
67
+ end
68
+
69
+ # @see Contract#transform!
70
+ def transform!(other)
71
+ @contracts.reduce(other) do |value, con|
72
+ con.transform!(value)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,53 @@
1
+ module Flows
2
+ class Contract
3
+ # Allows to combine two or more contracts with "or" logic.
4
+ #
5
+ # First matching contract from provided list will be used.
6
+ #
7
+ # @example
8
+ # str_or_sym = Flows::Contract::Either.new(String, Symbol)
9
+ #
10
+ # str_or_sym === 'AAA'
11
+ # # => true
12
+ #
13
+ # str_or_sym === :AAA
14
+ # # => true
15
+ #
16
+ # str_or_sym === 111
17
+ # # => false
18
+ class Either < Contract
19
+ # @param contracts [Array<Contract, Object>] contract list. Non-contract elements will be wrapped with {CaseEq}.
20
+ def initialize(*contracts)
21
+ raise 'Contract list must not be empty' if contracts.length.zero?
22
+
23
+ @contracts = contracts.map(&method(:to_contract))
24
+ end
25
+
26
+ # @see Contract#check!
27
+ def check!(other)
28
+ errors = @contracts.each_with_object([]) do |con, errs|
29
+ result = con.check(other)
30
+
31
+ return true if result.ok?
32
+
33
+ errs << result.error
34
+ end
35
+
36
+ raise Error.new(other, errors.join("\nOR "))
37
+ end
38
+
39
+ # @see Contract#transform!
40
+ def transform!(other)
41
+ errors = @contracts.each_with_object([]) do |con, errs|
42
+ result = con.transform(other)
43
+
44
+ return result.unwrap if result.ok?
45
+
46
+ errs << result.error
47
+ end
48
+
49
+ raise Error.new(other, errors.join("\nOR "))
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,24 @@
1
+ module Flows
2
+ class Contract
3
+ # Class for {Type} errors.
4
+ class Error < ::Flows::Error
5
+ attr_reader :value, :value_error
6
+
7
+ # @param value [Object] checked value
8
+ # @param value_error [String] error message
9
+ def initialize(value, value_error)
10
+ @value = value
11
+ @value_error = value_error
12
+ end
13
+
14
+ def message
15
+ [
16
+ 'type check failed for:',
17
+ " `#{@value.inspect}`",
18
+ "---\n",
19
+ @value_error
20
+ ].join("\n")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,75 @@
1
+ module Flows
2
+ class Contract
3
+ # Contract for Ruby `Hash` with specified contracts for keys and values.
4
+ #
5
+ # If key contract has transformation - Hash keys will be transformed.
6
+ #
7
+ # If value contract has transformation - Hash values will be transformed.
8
+ #
9
+ # @example
10
+ # sym_int_hash = Flows::Contract::Hash.new(Symbol, Integer)
11
+ #
12
+ # sym_int_hash === { a: 1, b: 2 }
13
+ # # => true
14
+ #
15
+ # sym_int_hash === { a: 1, b: 'BBB' }
16
+ # # => true
17
+ class Hash < Contract
18
+ # Stop search for a new type mismatch in keys or values
19
+ # if CHECK_LIMIT errors already found.
20
+ #
21
+ # Applied separately for keys and values.
22
+ CHECK_LIMIT = 10
23
+
24
+ HASH_TYPE = CaseEq.new(::Hash)
25
+
26
+ # @param key_contract [Contract, Object] contract for keys, non-contract values will be wrapped with {CaseEq}
27
+ # @param value_contract [Contract, Object] contract for values, non-contract values will be wrapped with {CaseEq}
28
+ def initialize(key_contract, value_contract)
29
+ @key_contract = to_contract(key_contract)
30
+ @value_contract = to_contract(value_contract)
31
+ end
32
+
33
+ def check!(other)
34
+ HASH_TYPE.check!(other)
35
+
36
+ unless other.keys.all?(&@key_contract) && other.values.all?(&@value_contract)
37
+ value_error = report_error(other)
38
+ raise Error.new(other, value_error)
39
+ end
40
+
41
+ true
42
+ end
43
+
44
+ def transform!(other)
45
+ check!(other)
46
+
47
+ other
48
+ .transform_keys { |key| @key_contract.transform!(key) }
49
+ .transform_values { |value| @value_contract.transform!(value) }
50
+ end
51
+
52
+ private
53
+
54
+ def report_error(other)
55
+ (invalid_key_errors(other) + invalid_value_errors(other)).join("\n")
56
+ end
57
+
58
+ def invalid_key_errors(other)
59
+ other.keys.reject(&@key_contract)[0..CHECK_LIMIT].map do |key|
60
+ key_error = @key_contract.check(key).error
61
+
62
+ merge_nested_errors("hash key `#{key.inspect}` is invalid:", key_error)
63
+ end
64
+ end
65
+
66
+ def invalid_value_errors(other)
67
+ other.values.reject(&@value_contract)[0..CHECK_LIMIT].map do |value|
68
+ value_error = @value_contract.check(value).error
69
+
70
+ merge_nested_errors("hash value `#{value.inspect}` is invalid:", value_error)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,70 @@
1
+ module Flows
2
+ class Contract
3
+ # Contract for Ruby `Hash` with specified structure.
4
+ #
5
+ # Hash can have extra keys. Extra keys will be removed after transform.
6
+ # Underlying contracts' transforms will be applied to correspond values.
7
+ #
8
+ # @example
9
+ # point_type = Flows::Contract::HashOf.new(x: Numeric, y: Numeric)
10
+ #
11
+ # point_type === { x: 1, y: 2.0 }
12
+ # # => true
13
+ #
14
+ # point_type === { x: 1, y: 2.0, name: 'Petr' }
15
+ # # => true
16
+ #
17
+ # point_type.cast(x: 1, y: 2.0, name: 'Petr').unwrap
18
+ # # => { x: 1, y: 2.0 }
19
+ #
20
+ # point_type.check({ x: 1, name: 'Petr' })
21
+ # # => Flows::Result::Error.new('missing key `:y`')
22
+ #
23
+ # point_type.check({ x: 1, y: 'Vasya' })
24
+ # # => Flows::Result::Error.new('key `:y` has an invalid value: must match `Numeric`')
25
+ class HashOf < Contract
26
+ HASH_CONTRACT = CaseEq.new(::Hash)
27
+
28
+ def initialize(shape = {})
29
+ @shape = shape.transform_values(&method(:to_contract))
30
+ end
31
+
32
+ def check!(other)
33
+ HASH_CONTRACT.check!(other)
34
+
35
+ errors = check_shape(other)
36
+
37
+ raise Error.new(other, errors.join("\n")) if errors.any?
38
+
39
+ true
40
+ end
41
+
42
+ def transform!(other)
43
+ check!(other)
44
+
45
+ other
46
+ .slice(*@shape.keys)
47
+ .map { |key, value| [key, @shape[key].transform!(value)] }
48
+ .to_h
49
+ end
50
+
51
+ private
52
+
53
+ # :reek:DuplicateMethodCall
54
+ def check_shape(other)
55
+ @shape.each_with_object([]) do |(key, type), errors|
56
+ unless other.key?(key)
57
+ errors << "missing hash key `#{key.inspect}`"
58
+ next
59
+ end
60
+
61
+ result = type.check(other[key])
62
+
63
+ if result.err?
64
+ errors << merge_nested_errors("hash key `#{key.inspect}` has an invalid assigned value:", result.error)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,22 @@
1
+ require 'forwardable'
2
+
3
+ module Flows
4
+ class Contract
5
+ # Shortcuts for contract creation.
6
+ module Helpers
7
+ extend Forwardable
8
+
9
+ def_delegator CaseEq, :new, :case_eq
10
+ def_delegator Predicate, :new, :predicate
11
+
12
+ def_delegator Transformer, :new, :transformer
13
+ def_delegator Compose, :new, :compose
14
+ def_delegator Either, :new, :either
15
+
16
+ def_delegator Flows::Contract::Hash, :new, :hash
17
+ def_delegator HashOf, :new, :hash_of
18
+ def_delegator Flows::Contract::Array, :new, :array
19
+ def_delegator Tuple, :new, :tuple
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,34 @@
1
+ module Flows
2
+ class Contract
3
+ # Makes a contract from 1-argument lambda.
4
+ #
5
+ # Such lambdas works like [predicates](https://en.wikipedia.org/wiki/Predicate_(mathematical_logic)).
6
+ #
7
+ # @example
8
+ # positive_check = Flows::Contract::Predicate.new 'must be a positive integer' do |x|
9
+ # x.is_a?(Integer) && x > 0
10
+ # end
11
+ #
12
+ # positive_check === 10
13
+ # # => true
14
+ #
15
+ # positive_check === -100
16
+ # # => false
17
+ class Predicate < Contract
18
+ # @param error_message error message if check fails
19
+ # @yield [object] lambda to wrap into a contract
20
+ # @yieldreturn [Boolean] lambda should return a boolean
21
+ def initialize(error_message, &block)
22
+ @error_message = error_message
23
+ @block = block
24
+ end
25
+
26
+ # @see Contract#check!
27
+ def check!(other)
28
+ raise Error.new(other, @error_message) unless @block === other
29
+
30
+ true
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,50 @@
1
+ module Flows
2
+ class Contract
3
+ # Adds transformation to an existing contract.
4
+ #
5
+ # If original contract already has a transform -
6
+ # final transformation will be composition of original and new one.
7
+ #
8
+ # You MUST obey Transformation Laws (see {Contract} documentation for details).
9
+ #
10
+ # @example Upcase strings
11
+ # up_str = Flows::Contract::Transformer.new(String) { |str| str.upcase }
12
+ #
13
+ # up_str.transform!('megatron')
14
+ # # => 'MEGATRON'
15
+ #
16
+ # up_str.transform(:megatron).error
17
+ # # => 'must match `String`'
18
+ #
19
+ # @example Strip and upcase strings
20
+ # strip_str = Flows::Contract::Transformer.new(String, &:strip)
21
+ # up_stip_str = Flows::Contract::Transformer.new(strip_str, &:upcase)
22
+ #
23
+ # up_str.transform!(' megatron ')
24
+ # # => 'MEGATRON'
25
+ #
26
+ # up_str.cast(:megatron).error
27
+ # # => 'must match `String`'
28
+ class Transformer < Contract
29
+ # @param contract [Contract, Object] in case of non-contract argument {CaseEq} is automatically applied.
30
+ # @yield [object] transform implementation
31
+ # @yieldreturn [object] result of transform. Must obey transformation laws.
32
+ def initialize(contract, &transform_proc)
33
+ @contract = to_contract(contract)
34
+ @transform = transform_proc
35
+ end
36
+
37
+ # @see Contract#check!
38
+ def check!(other)
39
+ @contract.check!(other)
40
+ end
41
+
42
+ # @see Contract#transform!
43
+ def transform!(other)
44
+ @transform.call(
45
+ @contract.transform!(other)
46
+ )
47
+ end
48
+ end
49
+ end
50
+ end