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.
- checksums.yaml +4 -4
- data/.github/workflows/{build.yml → test.yml} +5 -10
- data/.gitignore +9 -1
- data/.mdlrc +1 -1
- data/.reek.yml +54 -0
- data/.rubocop.yml +26 -7
- data/.rubocop_todo.yml +27 -0
- data/.ruby-version +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +81 -0
- data/Gemfile +0 -6
- data/README.md +167 -363
- data/Rakefile +35 -1
- data/bin/.rubocop.yml +5 -0
- data/bin/all_the_errors +55 -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 +22 -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 +138 -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/flow_error_demo.rb +22 -0
- data/bin/errors_cli/flows_router_error_demo.rb +15 -0
- data/bin/errors_cli/interface_error_demo.rb +17 -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 +3 -187
- data/docs/_sidebar.md +0 -24
- data/docs/index.html +1 -1
- data/flows.gemspec +27 -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 +43 -0
- data/lib/flows/contract/compose.rb +77 -0
- data/lib/flows/contract/either.rb +53 -0
- data/lib/flows/contract/error.rb +24 -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 +96 -7
- data/lib/flows/flow/errors.rb +29 -0
- data/lib/flows/flow/node.rb +132 -0
- data/lib/flows/flow/router.rb +29 -0
- data/lib/flows/flow/router/custom.rb +59 -0
- data/lib/flows/flow/router/errors.rb +11 -0
- data/lib/flows/flow/router/simple.rb +25 -0
- data/lib/flows/plugin.rb +15 -0
- data/lib/flows/plugin/dependency_injector.rb +170 -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 +55 -0
- data/lib/flows/plugin/dependency_injector/errors.rb +58 -0
- data/lib/flows/plugin/implicit_init.rb +45 -0
- data/lib/flows/plugin/interface.rb +84 -0
- data/lib/flows/plugin/output_contract.rb +85 -0
- data/lib/flows/plugin/output_contract/dsl.rb +48 -0
- data/lib/flows/plugin/output_contract/errors.rb +74 -0
- data/lib/flows/plugin/output_contract/wrapper.rb +55 -0
- data/lib/flows/plugin/profiler.rb +114 -0
- data/lib/flows/plugin/profiler/injector.rb +35 -0
- data/lib/flows/plugin/profiler/report.rb +48 -0
- data/lib/flows/plugin/profiler/report/events.rb +43 -0
- data/lib/flows/plugin/profiler/report/flat.rb +41 -0
- data/lib/flows/plugin/profiler/report/flat/method_report.rb +80 -0
- data/lib/flows/plugin/profiler/report/raw.rb +15 -0
- data/lib/flows/plugin/profiler/report/tree.rb +98 -0
- data/lib/flows/plugin/profiler/report/tree/calculated_node.rb +116 -0
- data/lib/flows/plugin/profiler/report/tree/node.rb +34 -0
- data/lib/flows/plugin/profiler/wrapper.rb +53 -0
- data/lib/flows/railway.rb +140 -34
- data/lib/flows/railway/dsl.rb +8 -18
- 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 +158 -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 +342 -0
- data/lib/flows/shared_context_pipeline/dsl.rb +12 -0
- data/lib/flows/shared_context_pipeline/dsl/callbacks.rb +35 -0
- data/lib/flows/shared_context_pipeline/dsl/tracks.rb +52 -0
- data/lib/flows/shared_context_pipeline/errors.rb +17 -0
- data/lib/flows/shared_context_pipeline/mutation_step.rb +30 -0
- data/lib/flows/shared_context_pipeline/router_definition.rb +21 -0
- data/lib/flows/shared_context_pipeline/step.rb +55 -0
- data/lib/flows/shared_context_pipeline/track.rb +54 -0
- data/lib/flows/shared_context_pipeline/track_list.rb +51 -0
- data/lib/flows/shared_context_pipeline/wrap.rb +73 -0
- data/lib/flows/util.rb +17 -0
- data/lib/flows/util/inheritable_singleton_vars.rb +86 -0
- data/lib/flows/util/inheritable_singleton_vars/dup_strategy.rb +100 -0
- data/lib/flows/util/inheritable_singleton_vars/isolation_strategy.rb +91 -0
- data/lib/flows/util/prepend_to_class.rb +191 -0
- data/lib/flows/version.rb +1 -1
- metadata +253 -38
- data/Gemfile.lock +0 -174
- 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/node.rb +0 -27
- data/lib/flows/operation.rb +0 -52
- 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
@@ -0,0 +1,25 @@
|
|
1
|
+
module Flows
|
2
|
+
class Flow
|
3
|
+
class Router
|
4
|
+
# Router with static paths for successful and failure results.
|
5
|
+
class Simple < Router
|
6
|
+
# @param success_route [Symbol] route for any successful results.
|
7
|
+
# @param failure_route [Symbol] route for any failure results.
|
8
|
+
def initialize(success_route, failure_route)
|
9
|
+
@success_route = success_route
|
10
|
+
@failure_route = failure_route
|
11
|
+
end
|
12
|
+
|
13
|
+
# @see Flows::Flow::Router#call
|
14
|
+
def call(result)
|
15
|
+
result.ok? ? @success_route : @failure_route
|
16
|
+
end
|
17
|
+
|
18
|
+
# @see Flows::Flow::Router#destinations
|
19
|
+
def destinations
|
20
|
+
[@success_route, @failure_route]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/flows/plugin.rb
ADDED
@@ -0,0 +1,15 @@
|
|
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'
|
14
|
+
require_relative 'plugin/profiler'
|
15
|
+
require_relative 'plugin/interface'
|
@@ -0,0 +1,170 @@
|
|
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
|
+
SingletonVarsSetup = Flows::Util::InheritableSingletonVars::DupStrategy.make_module(
|
92
|
+
'@dependencies' => {}
|
93
|
+
)
|
94
|
+
|
95
|
+
include SingletonVarsSetup
|
96
|
+
|
97
|
+
# @api private
|
98
|
+
module DSL
|
99
|
+
attr_reader :dependencies
|
100
|
+
|
101
|
+
# `:reek:BooleanParameter` disabled here because it's not applicable for DSLs
|
102
|
+
def dependency(name, required: false, default: NO_DEFAULT, type: NO_TYPE)
|
103
|
+
dependencies[name] = DependencyDefinition.new(
|
104
|
+
name: name,
|
105
|
+
required: required,
|
106
|
+
default: default,
|
107
|
+
type: type,
|
108
|
+
klass: self
|
109
|
+
)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# @api private
|
114
|
+
#
|
115
|
+
# `:reek:UtilityFunction` and `:reek:FeatureEnvy` are disabled here because Reek does not
|
116
|
+
# know about inheritance callback stuff.
|
117
|
+
module InheritanceCallback
|
118
|
+
def included(mod)
|
119
|
+
mod.extend(DSL)
|
120
|
+
|
121
|
+
mod.singleton_class.prepend(InheritanceCallback) if mod.class == Module
|
122
|
+
|
123
|
+
super
|
124
|
+
end
|
125
|
+
|
126
|
+
def extended(mod)
|
127
|
+
mod.extend(DSL)
|
128
|
+
|
129
|
+
mod.singleton_class.prepend(InheritanceCallback) if mod.class == Module
|
130
|
+
|
131
|
+
super
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
singleton_class.prepend InheritanceCallback
|
136
|
+
|
137
|
+
InitializerWrapper = Util::PrependToClass.make_module do
|
138
|
+
def initialize(*args, **kwargs, &block) # rubocop:disable Metrics/MethodLength
|
139
|
+
if @__dependencies_injected__
|
140
|
+
if kwargs.empty? # https://bugs.ruby-lang.org/issues/14415
|
141
|
+
super(*args, &block)
|
142
|
+
else
|
143
|
+
super(*args, **kwargs, &block)
|
144
|
+
end
|
145
|
+
|
146
|
+
return
|
147
|
+
end
|
148
|
+
@__dependencies_injected__ = true
|
149
|
+
|
150
|
+
klass = self.class
|
151
|
+
DependencyList.new(
|
152
|
+
klass: klass,
|
153
|
+
definitions: klass.dependencies,
|
154
|
+
provided_values: kwargs[:dependencies].dup || {}
|
155
|
+
).inject_to(self)
|
156
|
+
|
157
|
+
filtered_kwargs = kwargs.reject { |key, _| key == :dependencies }
|
158
|
+
|
159
|
+
if filtered_kwargs.empty? # https://bugs.ruby-lang.org/issues/14415
|
160
|
+
super(*args, &block)
|
161
|
+
else
|
162
|
+
super(*args, **filtered_kwargs, &block)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
include InitializerWrapper
|
168
|
+
end
|
169
|
+
end
|
170
|
+
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,55 @@
|
|
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, :provided_values, :dependencies
|
9
|
+
|
10
|
+
def initialize(klass:, definitions:, provided_values:)
|
11
|
+
@klass = klass
|
12
|
+
@definitions = definitions
|
13
|
+
@provided_values = provided_values.dup.tap { |pv| pv.default = NO_VALUE }
|
14
|
+
|
15
|
+
check_missing_dependencies
|
16
|
+
check_unexpected_dependencies
|
17
|
+
resolve_dependencies
|
18
|
+
end
|
19
|
+
|
20
|
+
def inject_to(instance)
|
21
|
+
dependencies.each { |dep| dep.inject_to(instance) }
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def required_dependencies
|
27
|
+
definitions.select { |_, definition| definition.required }.keys
|
28
|
+
end
|
29
|
+
|
30
|
+
def check_missing_dependencies
|
31
|
+
missing = required_dependencies - provided_values.keys
|
32
|
+
|
33
|
+
raise MissingDependencyError.new(@klass, missing) if missing.any?
|
34
|
+
end
|
35
|
+
|
36
|
+
def check_unexpected_dependencies
|
37
|
+
unexpected = provided_values.keys - definitions.keys
|
38
|
+
|
39
|
+
raise UnexpectedDependencyError.new(@klass, unexpected) if unexpected.any?
|
40
|
+
end
|
41
|
+
|
42
|
+
def resolve_dependencies
|
43
|
+
@dependencies = definitions.map do |name, definition|
|
44
|
+
Dependency.new(
|
45
|
+
klass: @klass,
|
46
|
+
name: name,
|
47
|
+
definition: definition,
|
48
|
+
provided_value: provided_values[name]
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
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
|
+
module Flows
|
2
|
+
module Plugin
|
3
|
+
# Class extension to define Java/C#-like interfaces in Ruby.
|
4
|
+
#
|
5
|
+
# On target class initialization will check defined methods for existence.
|
6
|
+
#
|
7
|
+
# **Currently interface composition is not supported.** You cannot define
|
8
|
+
# 2 interface modules and include it into one class.
|
9
|
+
#
|
10
|
+
# @example Simple interface
|
11
|
+
# class MyAction
|
12
|
+
# extend Flows::Plugin::Interface
|
13
|
+
#
|
14
|
+
# defmethod :perform
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# class InvalidAction < MyAction; end
|
18
|
+
# InvalidAction.new
|
19
|
+
# # will raise an error
|
20
|
+
#
|
21
|
+
# class ValidAction < MyAction
|
22
|
+
# def perfrom
|
23
|
+
# puts 'Hello!'
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
# ValidAction.new.perform
|
27
|
+
# # => Hello!
|
28
|
+
#
|
29
|
+
# @example Interface as module
|
30
|
+
# module MyBehavior
|
31
|
+
# extend Flows::Plugin::Interface
|
32
|
+
#
|
33
|
+
# defmethod :my_method
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# class MyImplementation
|
37
|
+
# include MyBehaviour
|
38
|
+
#
|
39
|
+
# def my_method; end
|
40
|
+
# end
|
41
|
+
module Interface
|
42
|
+
# Base error class for interface errors.
|
43
|
+
class Error < ::Flows::Error; end
|
44
|
+
|
45
|
+
# Raised when you're missed some dependency.
|
46
|
+
class MissingMethodsError < Error
|
47
|
+
def initialize(klass, names)
|
48
|
+
@klass = klass
|
49
|
+
@names = names
|
50
|
+
end
|
51
|
+
|
52
|
+
def message
|
53
|
+
"Methods required by interface for #{@klass} are missing: #{@names.map(&:to_s).join(', ')}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
SingletonVarsSetup = Flows::Util::InheritableSingletonVars::DupStrategy.make_module(
|
58
|
+
'@interface_methods' => {}
|
59
|
+
)
|
60
|
+
|
61
|
+
include SingletonVarsSetup
|
62
|
+
|
63
|
+
InitializePatch = Flows::Util::PrependToClass.make_module do
|
64
|
+
def initialize(*)
|
65
|
+
klass = self.class
|
66
|
+
|
67
|
+
required_methods = klass.instance_variable_get(:@interface_methods).keys
|
68
|
+
missing_methods = required_methods - methods
|
69
|
+
|
70
|
+
raise MissingMethodsError.new(klass, missing_methods) if missing_methods.any?
|
71
|
+
|
72
|
+
super
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
include InitializePatch
|
77
|
+
|
78
|
+
def defmethod(method_name)
|
79
|
+
method_list = instance_variable_get(:@interface_methods)
|
80
|
+
method_list[method_name.to_sym] = { required_by: self }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|