flows 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/{build.yml → test.yml} +5 -10
  3. data/.gitignore +1 -0
  4. data/.reek.yml +42 -0
  5. data/.rubocop.yml +20 -7
  6. data/.ruby-version +1 -1
  7. data/.yardopts +1 -0
  8. data/CHANGELOG.md +42 -0
  9. data/Gemfile +0 -6
  10. data/Gemfile.lock +139 -74
  11. data/README.md +158 -364
  12. data/Rakefile +35 -1
  13. data/bin/.rubocop.yml +5 -0
  14. data/bin/all_the_errors +47 -0
  15. data/bin/benchmark +73 -105
  16. data/bin/benchmark_cli/compare.rb +118 -0
  17. data/bin/benchmark_cli/compare/a_plus_b.rb +22 -0
  18. data/bin/benchmark_cli/compare/base.rb +45 -0
  19. data/bin/benchmark_cli/compare/command.rb +47 -0
  20. data/bin/benchmark_cli/compare/ten_steps.rb +22 -0
  21. data/bin/benchmark_cli/examples.rb +23 -0
  22. data/bin/benchmark_cli/examples/.rubocop.yml +19 -0
  23. data/bin/benchmark_cli/examples/a_plus_b/dry_do.rb +23 -0
  24. data/bin/benchmark_cli/examples/a_plus_b/dry_transaction.rb +17 -0
  25. data/bin/benchmark_cli/examples/a_plus_b/flows_do.rb +22 -0
  26. data/bin/benchmark_cli/examples/a_plus_b/flows_railway.rb +13 -0
  27. data/bin/benchmark_cli/examples/a_plus_b/flows_scp.rb +13 -0
  28. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_mut.rb +13 -0
  29. data/bin/benchmark_cli/examples/a_plus_b/flows_scp_oc.rb +21 -0
  30. data/bin/benchmark_cli/examples/a_plus_b/trailblazer.rb +15 -0
  31. data/bin/benchmark_cli/examples/ten_steps/dry_do.rb +70 -0
  32. data/bin/benchmark_cli/examples/ten_steps/dry_transaction.rb +64 -0
  33. data/bin/benchmark_cli/examples/ten_steps/flows_do.rb +69 -0
  34. data/bin/benchmark_cli/examples/ten_steps/flows_railway.rb +58 -0
  35. data/bin/benchmark_cli/examples/ten_steps/flows_scp.rb +58 -0
  36. data/bin/benchmark_cli/examples/ten_steps/flows_scp_mut.rb +58 -0
  37. data/bin/benchmark_cli/examples/ten_steps/flows_scp_oc.rb +66 -0
  38. data/bin/benchmark_cli/examples/ten_steps/trailblazer.rb +60 -0
  39. data/bin/benchmark_cli/helpers.rb +12 -0
  40. data/bin/benchmark_cli/ruby.rb +15 -0
  41. data/bin/benchmark_cli/ruby/command.rb +38 -0
  42. data/bin/benchmark_cli/ruby/method_exec.rb +71 -0
  43. data/bin/benchmark_cli/ruby/self_class.rb +69 -0
  44. data/bin/benchmark_cli/ruby/structs.rb +90 -0
  45. data/bin/console +1 -0
  46. data/bin/docserver +7 -0
  47. data/bin/errors +118 -0
  48. data/bin/errors_cli/contract_error_demo.rb +49 -0
  49. data/bin/errors_cli/di_error_demo.rb +38 -0
  50. data/bin/errors_cli/flows_router_error_demo.rb +15 -0
  51. data/bin/errors_cli/oc_error_demo.rb +40 -0
  52. data/bin/errors_cli/railway_error_demo.rb +10 -0
  53. data/bin/errors_cli/result_error_demo.rb +13 -0
  54. data/bin/errors_cli/scp_error_demo.rb +17 -0
  55. data/docs/README.md +2 -186
  56. data/docs/_sidebar.md +0 -24
  57. data/docs/index.html +1 -1
  58. data/flows.gemspec +25 -2
  59. data/forspell.dict +9 -0
  60. data/lefthook.yml +9 -0
  61. data/lib/flows.rb +11 -5
  62. data/lib/flows/contract.rb +402 -0
  63. data/lib/flows/contract/array.rb +55 -0
  64. data/lib/flows/contract/case_eq.rb +41 -0
  65. data/lib/flows/contract/compose.rb +77 -0
  66. data/lib/flows/contract/either.rb +53 -0
  67. data/lib/flows/contract/error.rb +25 -0
  68. data/lib/flows/contract/hash.rb +75 -0
  69. data/lib/flows/contract/hash_of.rb +70 -0
  70. data/lib/flows/contract/helpers.rb +22 -0
  71. data/lib/flows/contract/predicate.rb +34 -0
  72. data/lib/flows/contract/transformer.rb +50 -0
  73. data/lib/flows/contract/tuple.rb +70 -0
  74. data/lib/flows/flow.rb +75 -7
  75. data/lib/flows/flow/node.rb +131 -0
  76. data/lib/flows/flow/router.rb +25 -0
  77. data/lib/flows/flow/router/custom.rb +54 -0
  78. data/lib/flows/flow/router/errors.rb +11 -0
  79. data/lib/flows/flow/router/simple.rb +20 -0
  80. data/lib/flows/plugin.rb +13 -0
  81. data/lib/flows/plugin/dependency_injector.rb +159 -0
  82. data/lib/flows/plugin/dependency_injector/dependency.rb +24 -0
  83. data/lib/flows/plugin/dependency_injector/dependency_definition.rb +16 -0
  84. data/lib/flows/plugin/dependency_injector/dependency_list.rb +57 -0
  85. data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
  86. data/lib/flows/plugin/implicit_init.rb +45 -0
  87. data/lib/flows/plugin/output_contract.rb +84 -0
  88. data/lib/flows/plugin/output_contract/dsl.rb +36 -0
  89. data/lib/flows/plugin/output_contract/errors.rb +74 -0
  90. data/lib/flows/plugin/output_contract/wrapper.rb +53 -0
  91. data/lib/flows/railway.rb +140 -37
  92. data/lib/flows/railway/dsl.rb +8 -19
  93. data/lib/flows/railway/errors.rb +8 -12
  94. data/lib/flows/railway/step.rb +24 -0
  95. data/lib/flows/railway/step_list.rb +38 -0
  96. data/lib/flows/result.rb +188 -2
  97. data/lib/flows/result/do.rb +160 -16
  98. data/lib/flows/result/err.rb +12 -6
  99. data/lib/flows/result/errors.rb +29 -17
  100. data/lib/flows/result/helpers.rb +25 -3
  101. data/lib/flows/result/ok.rb +12 -6
  102. data/lib/flows/shared_context_pipeline.rb +216 -0
  103. data/lib/flows/shared_context_pipeline/dsl.rb +63 -0
  104. data/lib/flows/shared_context_pipeline/errors.rb +17 -0
  105. data/lib/flows/shared_context_pipeline/mutation_step.rb +31 -0
  106. data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
  107. data/lib/flows/shared_context_pipeline/step.rb +46 -0
  108. data/lib/flows/shared_context_pipeline/track.rb +67 -0
  109. data/lib/flows/shared_context_pipeline/track_list.rb +46 -0
  110. data/lib/flows/util.rb +17 -0
  111. data/lib/flows/util/inheritable_singleton_vars.rb +79 -0
  112. data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +109 -0
  113. data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +104 -0
  114. data/lib/flows/util/prepend_to_class.rb +145 -0
  115. data/lib/flows/version.rb +1 -1
  116. metadata +233 -37
  117. data/bin/demo +0 -66
  118. data/bin/examples.rb +0 -195
  119. data/bin/profile_10steps +0 -106
  120. data/bin/ruby_benchmarks +0 -26
  121. data/docs/CNAME +0 -1
  122. data/docs/contributing/benchmarks_profiling.md +0 -3
  123. data/docs/contributing/local_development.md +0 -3
  124. data/docs/flow/direct_usage.md +0 -3
  125. data/docs/flow/general_idea.md +0 -3
  126. data/docs/operation/basic_usage.md +0 -1
  127. data/docs/operation/inject_steps.md +0 -3
  128. data/docs/operation/lambda_steps.md +0 -3
  129. data/docs/operation/result_shapes.md +0 -3
  130. data/docs/operation/routing_tracks.md +0 -3
  131. data/docs/operation/wrapping_steps.md +0 -3
  132. data/docs/overview/performance.md +0 -336
  133. data/docs/railway/basic_usage.md +0 -232
  134. data/docs/result_objects/basic_usage.md +0 -196
  135. data/docs/result_objects/do_notation.md +0 -139
  136. data/lib/flows/implicit_build.rb +0 -16
  137. data/lib/flows/node.rb +0 -27
  138. data/lib/flows/operation.rb +0 -55
  139. data/lib/flows/operation/builder.rb +0 -130
  140. data/lib/flows/operation/builder/build_router.rb +0 -37
  141. data/lib/flows/operation/dsl.rb +0 -93
  142. data/lib/flows/operation/errors.rb +0 -75
  143. data/lib/flows/operation/executor.rb +0 -78
  144. data/lib/flows/railway/builder.rb +0 -68
  145. data/lib/flows/railway/executor.rb +0 -23
  146. data/lib/flows/result_router.rb +0 -14
  147. data/lib/flows/router.rb +0 -22
@@ -0,0 +1,13 @@
1
+ module Flows
2
+ # Namespace for class behaviour extensions.
3
+ #
4
+ # Feel free to use it to empower your abstractions.
5
+ #
6
+ # @since 0.4.0
7
+ module Plugin
8
+ end
9
+ end
10
+
11
+ require_relative 'plugin/dependency_injector'
12
+ require_relative 'plugin/implicit_init'
13
+ require_relative 'plugin/output_contract'
@@ -0,0 +1,159 @@
1
+ require_relative 'dependency_injector/errors'
2
+ require_relative 'dependency_injector/dependency'
3
+ require_relative 'dependency_injector/dependency_definition'
4
+ require_relative 'dependency_injector/dependency_list'
5
+
6
+ module Flows
7
+ module Plugin
8
+ # Allows to inject dependencies on the initialization step.
9
+ #
10
+ # After including this module you inject dependencies by providing `:dependencies` key
11
+ # to your initializer:
12
+ #
13
+ # x = MyClass.new(dependencies: { my_dep: -> { 'Hi' } })
14
+ # x.my_dep
15
+ # # => 'Hi'
16
+ #
17
+ # Keys are dependency names. Dependency will be injected as
18
+ # a public method with dependency name. Values are dependencies itself.
19
+ #
20
+ # You can also require some dependencies to be present.
21
+ # If required dependency is missed - {MissingDependencyError} will be raised.
22
+ #
23
+ # If an optional dependency has no default - {MissingDependencyDefaultError} will be raised.
24
+ #
25
+ # For an optional dependency default value must be provided.
26
+ #
27
+ # You can provide a type for the dependency.
28
+ # Type check uses case equality (`===`).
29
+ # So, it works like Ruby's `case`.
30
+ # In case of type mismatch {UnexpectedDependencyTypeError} will be raised.
31
+ #
32
+ # dependency :name, type: String # name should be a string
33
+ #
34
+ # # by the way, you can use lambdas like in Ruby's `case`
35
+ # dependency :age, type: ->(x) { x.is_a?(Number) && x > 0 && x < 100 }
36
+ #
37
+ # If you're trying to inject undeclared dependency - {UnexpectedDependencyError} will be raised.
38
+ #
39
+ # Inheritance is supported and dependency definitions will be inherited into child classes.
40
+ #
41
+ # @example
42
+ #
43
+ # class MyClass
44
+ # include Flows::Plugin::DependencyInjector
45
+ #
46
+ # dependency :logger, required: true
47
+ # dependency :name, default: 'Boris', type: String # by default dependency is optional.
48
+ #
49
+ # attr_reader :data
50
+ #
51
+ # def initializer(data)
52
+ # @data = data
53
+ # end
54
+ #
55
+ # def log_the_name
56
+ # logger.call(name)
57
+ # end
58
+ # end
59
+ #
60
+ # class Logger
61
+ # def self.call(msg)
62
+ # puts msg
63
+ # end
64
+ # end
65
+ #
66
+ # x = MyClass.new('DATA', dependencies: {
67
+ # logger: Logger
68
+ # })
69
+ #
70
+ # x.data
71
+ # # => 'DATA'
72
+ #
73
+ # x.name
74
+ # # => 'Boris'
75
+ #
76
+ # x.logger.call('Hello')
77
+ # # prints 'Hello'
78
+ #
79
+ # x.log_the_name
80
+ # # prints 'Boris'
81
+ module DependencyInjector
82
+ # Placeholder for empty type. We cannot use `nil` because value can be `nil`.
83
+ NO_TYPE = :__no_type__
84
+
85
+ # Placeholder for empty default. We cannot use `nil` because value can be `nil`.
86
+ NO_DEFAULT = :__no_default__
87
+
88
+ # Placeholder for empty value. We cannot use `nil` because value can be `nil`.
89
+ NO_VALUE = :__no_value__
90
+
91
+ Flows::Util::InheritableSingletonVars::DupStrategy.call(
92
+ self,
93
+ '@dependencies' => {}
94
+ )
95
+
96
+ # @api private
97
+ module DSL
98
+ attr_reader :dependencies
99
+
100
+ # `:reek:BooleanParameter` disabled here because it's not applicable for DSLs
101
+ def dependency(name, required: false, default: NO_DEFAULT, type: NO_TYPE)
102
+ dependencies[name] = DependencyDefinition.new(
103
+ name: name,
104
+ required: required,
105
+ default: default,
106
+ type: type,
107
+ klass: self
108
+ )
109
+ end
110
+ end
111
+
112
+ # @api private
113
+ #
114
+ # `:reek:UtilityFunction` and `:reek:FeatureEnvy` are disabled here because Reek does not
115
+ # know about inheritance callback stuff.
116
+ module InheritanceCallback
117
+ def included(mod)
118
+ mod.extend(DSL)
119
+
120
+ mod.singleton_class.prepend(InheritanceCallback) if mod.class == Module
121
+
122
+ super
123
+ end
124
+
125
+ def extended(mod)
126
+ mod.extend(DSL)
127
+
128
+ mod.singleton_class.prepend(InheritanceCallback) if mod.class == Module
129
+
130
+ super
131
+ end
132
+ end
133
+
134
+ singleton_class.prepend InheritanceCallback
135
+
136
+ # @api private
137
+ module InitializePatch
138
+ def initialize(*args, **kwargs, &block) # rubocop:disable Metrics/MethodLength
139
+ klass = self.class
140
+ DependencyList.new(
141
+ klass: klass,
142
+ definitions: klass.dependencies,
143
+ provided_values: kwargs[:dependencies].dup || {}
144
+ ).inject_to(self)
145
+
146
+ filtered_kwargs = kwargs.reject { |key, _| key == :dependencies }
147
+
148
+ if filtered_kwargs.empty? # https://bugs.ruby-lang.org/issues/14415
149
+ super(*args, &block)
150
+ else
151
+ super(*args, **filtered_kwargs, &block)
152
+ end
153
+ end
154
+ end
155
+
156
+ Flows::Util::PrependToClass.call(self, InitializePatch)
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,24 @@
1
+ module Flows
2
+ module Plugin
3
+ module DependencyInjector
4
+ # Resolves dependency on initialization and can inject it into class instance.
5
+ #
6
+ # @api private
7
+ Dependency = Struct.new(:name, :definition, :provided_value, :value, :klass, keyword_init: true) do
8
+ def initialize(*)
9
+ super
10
+
11
+ self.value = provided_value == NO_VALUE ? definition.default : provided_value
12
+ type = definition.type
13
+
14
+ raise UnexpectedDependencyTypeError.new(klass, name, value, type) if type != NO_TYPE && !(type === value) # rubocop:disable Style/CaseEquality
15
+ end
16
+
17
+ def inject_to(instance)
18
+ value = self.value
19
+ instance.define_singleton_method(name) { value }
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ module Flows
2
+ module Plugin
3
+ module DependencyInjector
4
+ # Struct for storing dependency definitions.
5
+ #
6
+ # @api private
7
+ DependencyDefinition = Struct.new(:name, :required, :default, :type, :klass, keyword_init: true) do
8
+ def initialize(*)
9
+ super
10
+
11
+ raise MissingDependencyDefaultError.new(klass, name) if !required && (default == NO_DEFAULT)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,57 @@
1
+ module Flows
2
+ module Plugin
3
+ module DependencyInjector
4
+ # Resolves dependencies on initialization and can inject it into class instance.
5
+ #
6
+ # @api private
7
+ class DependencyList
8
+ attr_reader :definitions
9
+ attr_reader :provided_values
10
+ attr_reader :dependencies
11
+
12
+ def initialize(klass:, definitions:, provided_values:)
13
+ @klass = klass
14
+ @definitions = definitions
15
+ @provided_values = provided_values.dup.tap { |pv| pv.default = NO_VALUE }
16
+
17
+ check_missing_dependencies
18
+ check_unexpected_dependencies
19
+ resolve_dependencies
20
+ end
21
+
22
+ def inject_to(instance)
23
+ dependencies.each { |dep| dep.inject_to(instance) }
24
+ end
25
+
26
+ private
27
+
28
+ def required_dependencies
29
+ definitions.select { |_, definition| definition.required }.keys
30
+ end
31
+
32
+ def check_missing_dependencies
33
+ missing = required_dependencies - provided_values.keys
34
+
35
+ raise MissingDependencyError.new(@klass, missing) if missing.any?
36
+ end
37
+
38
+ def check_unexpected_dependencies
39
+ unexpected = provided_values.keys - definitions.keys
40
+
41
+ raise UnexpectedDependencyError.new(@klass, unexpected) if unexpected.any?
42
+ end
43
+
44
+ def resolve_dependencies
45
+ @dependencies = definitions.map do |name, definition|
46
+ Dependency.new(
47
+ klass: @klass,
48
+ name: name,
49
+ definition: definition,
50
+ provided_value: provided_values[name]
51
+ )
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,58 @@
1
+ module Flows
2
+ module Plugin
3
+ module DependencyInjector
4
+ # Base error class for dependency injection errors.
5
+ class Error < ::Flows::Error; end
6
+
7
+ # Raised when you're missed some dependency.
8
+ class MissingDependencyError < Error
9
+ def initialize(klass, names)
10
+ @klass = klass
11
+ @names = names
12
+ end
13
+
14
+ def message
15
+ "Missing dependency(ies) for #{@klass}: #{@names.map(&:to_s).join(', ')}"
16
+ end
17
+ end
18
+
19
+ # Raised when you're providing undeclared dependency.
20
+ class UnexpectedDependencyError < Error
21
+ def initialize(klass, names)
22
+ @klass = klass
23
+ @names = names
24
+ end
25
+
26
+ def message
27
+ "Unexpected dependency(ies) for #{@klass}: #{@names.map(&:to_s).join(', ')}"
28
+ end
29
+ end
30
+
31
+ # Raised when dependency has unexpected type.
32
+ class UnexpectedDependencyTypeError < Error
33
+ def initialize(klass, name, value, type)
34
+ @klass = klass
35
+ @_name = name
36
+ @value = value
37
+ @_type = type
38
+ end
39
+
40
+ def message
41
+ "#{@_name} dependency for #{@klass} has wrong type, must conform `#{@_type.inspect}`: `#{@value.inspect}`"
42
+ end
43
+ end
44
+
45
+ # Raised when an optional dependency has no default value.
46
+ class MissingDependencyDefaultError < Error
47
+ def initialize(klass, name)
48
+ @klass = klass
49
+ @_name = name
50
+ end
51
+
52
+ def message
53
+ "Optional dependency #{@_name} for #{@klass} has no default value"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,45 @@
1
+ module Flows
2
+ module Plugin
3
+ # Class extension with method `MyClass.call` which works like `MyClass.new.call`.
4
+ #
5
+ # @note This module must be injected into target class using `extend`, not `include`.
6
+ #
7
+ # @note Class inheritance is supported: each child class will inherit behaviour, but not data.
8
+ #
9
+ # @example Extending a class
10
+ # class SomeClass
11
+ # extend Flows::Plugin::ImplicitInit
12
+ #
13
+ # def initialize(param: 'default')
14
+ # @param = param
15
+ # end
16
+ #
17
+ # def call
18
+ # @param
19
+ # end
20
+ # end
21
+ #
22
+ # SomeClass.call
23
+ # # => 'default'
24
+ #
25
+ # SomeClass.default_instance.call
26
+ # # => 'default'
27
+ # @since 0.4.0
28
+ module ImplicitInit
29
+ # Contains memoized instance of a host class or `nil`.
30
+ attr_reader :default_instance
31
+
32
+ # Creates an instance of a host class by calling `new` without arguments and
33
+ # calls `#call` method on the instance with provided parameters and block.
34
+ #
35
+ # After first invocation the instance will be memoized in {.default_instance}.
36
+ #
37
+ # Child classes have separate default instances.
38
+ def call(*args, **kwargs, &block)
39
+ @default_instance ||= new
40
+
41
+ default_instance.call(*args, **kwargs, &block)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,84 @@
1
+ require_relative 'output_contract/errors'
2
+ require_relative 'output_contract/dsl'
3
+ require_relative 'output_contract/wrapper'
4
+
5
+ module Flows
6
+ module Plugin
7
+ # Allows to make a contract check and transformation for `#call` method execution in any class.
8
+ #
9
+ # Plugin applies a wrapper to a `#call` instance method.
10
+ # This wrapper will do the following:
11
+ #
12
+ # * check that {Result} instance is returned by `#call`
13
+ # * check that returned {Result#status} is expected
14
+ # * check that returned result data conforms {Contract} assigned
15
+ # to a particular result type and status
16
+ # * applies contract transform to the returned data
17
+ # * returns {Result} with the same status and type,
18
+ # wraps transformed data inside.
19
+ #
20
+ # Plugin provides DSL to express expected result statuses and assigned contracts.
21
+ # Contracts definition reuses {Contract.make} to execute block and get a contract.
22
+ #
23
+ # * `success_with(status, &block)` - defines contract for a successful result with status `status`.
24
+ # * `failure_with(status, &block)` - defines contract for a failure result with status `status`.
25
+ #
26
+ # @example with one possible output contract
27
+ # class DoJob
28
+ # include Flows::Result::Helpers
29
+ # include Flows::Plugin::OutputContract
30
+ #
31
+ # success_with :ok do
32
+ # Integer
33
+ # end
34
+ #
35
+ # def call(a, b)
36
+ # ok_data(a + b)
37
+ # end
38
+ # end
39
+ #
40
+ # DoJob.new.call(1, 2).unwrap
41
+ # # => 3
42
+ #
43
+ # DoJob.new.call('a', 'b')
44
+ # # Flows::Contract::Error exception raised
45
+ #
46
+ # @example with multiple contracts
47
+ # class DoJob
48
+ # include Flows::Result::Helpers
49
+ # include Flows::Plugin::OutputContract
50
+ #
51
+ # success_with :int_sum do
52
+ # Integer
53
+ # end
54
+ #
55
+ # success_with :float_sum do
56
+ # Float
57
+ # end
58
+ #
59
+ # failure_with :err do
60
+ # hash_of(
61
+ # key: Symbol,
62
+ # msg: String
63
+ # )
64
+ # end
65
+ #
66
+ # def call(a, b)
67
+ # if a.is_a?(Float) || b.is_a?(Float)
68
+ # ok_data(a + b, status: :float_sum)
69
+ # elsif a.is_a?(Integer) && b.is_a?(Integer)
70
+ # ok_data(a + b, status: :int_sum)
71
+ # else
72
+ # err(key: :unexpected_type, msg: "Unexpected argument types")
73
+ # end
74
+ # end
75
+ # end
76
+ module OutputContract
77
+ # @api private
78
+ def self.included(mod)
79
+ mod.extend(DSL)
80
+ mod.prepend(Wrapper)
81
+ end
82
+ end
83
+ end
84
+ end