flows 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|