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.
- checksums.yaml +4 -4
- data/.github/workflows/{build.yml → test.yml} +5 -10
- data/.gitignore +1 -0
- data/.reek.yml +42 -0
- data/.rubocop.yml +20 -7
- data/.ruby-version +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +42 -0
- data/Gemfile +0 -6
- data/Gemfile.lock +139 -74
- data/README.md +158 -364
- data/Rakefile +35 -1
- data/bin/.rubocop.yml +5 -0
- data/bin/all_the_errors +47 -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 +19 -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 +118 -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/flows_router_error_demo.rb +15 -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 +2 -186
- data/docs/_sidebar.md +0 -24
- data/docs/index.html +1 -1
- data/flows.gemspec +25 -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 +41 -0
- data/lib/flows/contract/compose.rb +77 -0
- data/lib/flows/contract/either.rb +53 -0
- data/lib/flows/contract/error.rb +25 -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 +75 -7
- data/lib/flows/flow/node.rb +131 -0
- data/lib/flows/flow/router.rb +25 -0
- data/lib/flows/flow/router/custom.rb +54 -0
- data/lib/flows/flow/router/errors.rb +11 -0
- data/lib/flows/flow/router/simple.rb +20 -0
- data/lib/flows/plugin.rb +13 -0
- data/lib/flows/plugin/dependency_injector.rb +159 -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 +57 -0
- data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
- data/lib/flows/plugin/implicit_init.rb +45 -0
- data/lib/flows/plugin/output_contract.rb +84 -0
- data/lib/flows/plugin/output_contract/dsl.rb +36 -0
- data/lib/flows/plugin/output_contract/errors.rb +74 -0
- data/lib/flows/plugin/output_contract/wrapper.rb +53 -0
- data/lib/flows/railway.rb +140 -37
- data/lib/flows/railway/dsl.rb +8 -19
- 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 +160 -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 +216 -0
- data/lib/flows/shared_context_pipeline/dsl.rb +63 -0
- data/lib/flows/shared_context_pipeline/errors.rb +17 -0
- data/lib/flows/shared_context_pipeline/mutation_step.rb +31 -0
- data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
- data/lib/flows/shared_context_pipeline/step.rb +46 -0
- data/lib/flows/shared_context_pipeline/track.rb +67 -0
- data/lib/flows/shared_context_pipeline/track_list.rb +46 -0
- data/lib/flows/util.rb +17 -0
- data/lib/flows/util/inheritable_singleton_vars.rb +79 -0
- data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +109 -0
- data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +104 -0
- data/lib/flows/util/prepend_to_class.rb +145 -0
- data/lib/flows/version.rb +1 -1
- metadata +233 -37
- 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/implicit_build.rb +0 -16
- data/lib/flows/node.rb +0 -27
- data/lib/flows/operation.rb +0 -55
- data/lib/flows/operation/builder.rb +0 -130
- data/lib/flows/operation/builder/build_router.rb +0 -37
- data/lib/flows/operation/dsl.rb +0 -93
- data/lib/flows/operation/errors.rb +0 -75
- data/lib/flows/operation/executor.rb +0 -78
- data/lib/flows/railway/builder.rb +0 -68
- data/lib/flows/railway/executor.rb +0 -23
- data/lib/flows/result_router.rb +0 -14
- data/lib/flows/router.rb +0 -22
data/lib/flows/plugin.rb
ADDED
@@ -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
|