dry-auto_inject 0.7.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 +7 -0
- data/.codeclimate.yml +12 -0
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
- data/.github/ISSUE_TEMPLATE/---bug-report.md +30 -0
- data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
- data/.github/workflows/ci.yml +76 -0
- data/.github/workflows/docsite.yml +34 -0
- data/.github/workflows/sync_configs.yml +34 -0
- data/.gitignore +10 -0
- data/.rspec +4 -0
- data/.rubocop.yml +95 -0
- data/.rubocop_todo.yml +6 -0
- data/CHANGELOG.md +257 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/CONTRIBUTING.md +29 -0
- data/Gemfile +17 -0
- data/LICENSE +20 -0
- data/README.md +58 -0
- data/Rakefile +14 -0
- data/bin/console +11 -0
- data/bin/setup +7 -0
- data/docsite/source/basic-usage.html.md +104 -0
- data/docsite/source/how-does-it-work.html.md +45 -0
- data/docsite/source/index.html.md +53 -0
- data/docsite/source/injection-strategies.html.md +94 -0
- data/dry-auto_inject.gemspec +29 -0
- data/lib/dry-auto_inject.rb +3 -0
- data/lib/dry/auto_inject.rb +46 -0
- data/lib/dry/auto_inject/builder.rb +40 -0
- data/lib/dry/auto_inject/dependency_map.rb +55 -0
- data/lib/dry/auto_inject/injector.rb +39 -0
- data/lib/dry/auto_inject/method_parameters.rb +92 -0
- data/lib/dry/auto_inject/strategies.rb +21 -0
- data/lib/dry/auto_inject/strategies/args.rb +68 -0
- data/lib/dry/auto_inject/strategies/constructor.rb +56 -0
- data/lib/dry/auto_inject/strategies/hash.rb +41 -0
- data/lib/dry/auto_inject/strategies/kwargs.rb +105 -0
- data/lib/dry/auto_inject/version.rb +7 -0
- data/rakelib/rubocop.rake +20 -0
- metadata +137 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'dry/auto_inject/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'dry-auto_inject'
|
9
|
+
spec.version = Dry::AutoInject::VERSION.dup
|
10
|
+
spec.authors = ['Piotr Solnica']
|
11
|
+
spec.email = ['piotr.solnica@gmail.com']
|
12
|
+
spec.license = 'MIT'
|
13
|
+
|
14
|
+
spec.summary = 'Container-agnostic automatic constructor injection'
|
15
|
+
spec.homepage = 'https://github.com/dry-rb/dry-auto_inject'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = 'exe'
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.required_ruby_version = '>= 2.4.0'
|
23
|
+
|
24
|
+
spec.add_runtime_dependency 'dry-container', '>= 0.3.4'
|
25
|
+
|
26
|
+
spec.add_development_dependency 'bundler'
|
27
|
+
spec.add_development_dependency 'rake'
|
28
|
+
spec.add_development_dependency 'rspec', '~> 3.8'
|
29
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/auto_inject/builder'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
# Configure an auto-injection module
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# module MyApp
|
10
|
+
# # set up your container
|
11
|
+
# container = Dry::Container.new
|
12
|
+
#
|
13
|
+
# container.register(:data_store, -> { DataStore.new })
|
14
|
+
# container.register(:user_repository, -> { container[:data_store][:users] })
|
15
|
+
# container.register(:persist_user, -> { PersistUser.new })
|
16
|
+
#
|
17
|
+
# # set up your auto-injection function
|
18
|
+
# AutoInject = Dry::AutoInject(container)
|
19
|
+
#
|
20
|
+
# # define your injection function
|
21
|
+
# def self.Inject(*keys)
|
22
|
+
# AutoInject[*keys]
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# # then simply include it in your class providing which dependencies should be
|
27
|
+
# # injected automatically from the configured container
|
28
|
+
# class PersistUser
|
29
|
+
# include MyApp::Inject(:user_repository)
|
30
|
+
#
|
31
|
+
# def call(user)
|
32
|
+
# user_repository << user
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# persist_user = container[:persist_user]
|
37
|
+
#
|
38
|
+
# persist_user.call(name: 'Jane')
|
39
|
+
#
|
40
|
+
# @return [Proc] calling the returned proc builds an auto-injection module
|
41
|
+
#
|
42
|
+
# @api public
|
43
|
+
def self.AutoInject(container, options = {})
|
44
|
+
AutoInject::Builder.new(container, options)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/auto_inject/strategies'
|
4
|
+
require 'dry/auto_inject/injector'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module AutoInject
|
8
|
+
class Builder < BasicObject
|
9
|
+
# @api private
|
10
|
+
attr_reader :container
|
11
|
+
|
12
|
+
# @api private
|
13
|
+
attr_reader :strategies
|
14
|
+
|
15
|
+
def initialize(container, options = {})
|
16
|
+
@container = container
|
17
|
+
@strategies = options.fetch(:strategies) { Strategies }
|
18
|
+
end
|
19
|
+
|
20
|
+
# @api public
|
21
|
+
def [](*dependency_names)
|
22
|
+
default[*dependency_names]
|
23
|
+
end
|
24
|
+
|
25
|
+
def respond_to?(name, include_private = false)
|
26
|
+
Builder.public_instance_methods.include?(name) || strategies.key?(name)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def method_missing(name, *args, &block)
|
32
|
+
if strategies.key?(name)
|
33
|
+
Injector.new(container, strategies[name], builder: self)
|
34
|
+
else
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module AutoInject
|
5
|
+
DuplicateDependencyError = Class.new(StandardError)
|
6
|
+
DependencyNameInvalid = Class.new(StandardError)
|
7
|
+
|
8
|
+
VALID_NAME = /([a-z_][a-zA-Z_0-9]*)$/
|
9
|
+
|
10
|
+
class DependencyMap
|
11
|
+
def initialize(*dependencies)
|
12
|
+
@map = {}
|
13
|
+
|
14
|
+
dependencies = dependencies.dup
|
15
|
+
aliases = dependencies.last.is_a?(Hash) ? dependencies.pop : {}
|
16
|
+
|
17
|
+
dependencies.each do |identifier|
|
18
|
+
name = name_for(identifier)
|
19
|
+
add_dependency(name, identifier)
|
20
|
+
end
|
21
|
+
|
22
|
+
aliases.each do |name, identifier|
|
23
|
+
add_dependency(name, identifier)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def inspect
|
28
|
+
@map.inspect
|
29
|
+
end
|
30
|
+
|
31
|
+
def names
|
32
|
+
@names ||= @map.keys
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_h
|
36
|
+
@map.dup
|
37
|
+
end
|
38
|
+
alias_method :to_hash, :to_h
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def name_for(identifier)
|
43
|
+
matched = VALID_NAME.match(identifier.to_s)
|
44
|
+
raise DependencyNameInvalid, "name +#{identifier}+ is not a valid Ruby identifier" unless matched
|
45
|
+
matched[0]
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_dependency(name, identifier)
|
49
|
+
name = name.to_sym
|
50
|
+
raise DuplicateDependencyError, "name +#{name}+ is already used" if @map.key?(name)
|
51
|
+
@map[name] = identifier
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/auto_inject/strategies'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module AutoInject
|
7
|
+
class Injector < BasicObject
|
8
|
+
# @api private
|
9
|
+
attr_reader :container
|
10
|
+
|
11
|
+
# @api private
|
12
|
+
attr_reader :strategy
|
13
|
+
|
14
|
+
# @api private
|
15
|
+
attr_reader :builder
|
16
|
+
|
17
|
+
# @api private
|
18
|
+
def initialize(container, strategy, builder:)
|
19
|
+
@container = container
|
20
|
+
@strategy = strategy
|
21
|
+
@builder = builder
|
22
|
+
end
|
23
|
+
|
24
|
+
def [](*dependency_names)
|
25
|
+
strategy.new(container, *dependency_names)
|
26
|
+
end
|
27
|
+
|
28
|
+
def respond_to?(name, include_private = false)
|
29
|
+
Injector.instance_methods.include?(name) || builder.respond_to?(name)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def method_missing(name, *args, &block)
|
35
|
+
builder.__send__(name)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module AutoInject
|
5
|
+
# @api private
|
6
|
+
class MethodParameters
|
7
|
+
PASS_THROUGH = [[:rest]]
|
8
|
+
|
9
|
+
if RUBY_VERSION >= '2.4.4.' && !defined? JRUBY_VERSION
|
10
|
+
def self.of(obj, name)
|
11
|
+
Enumerator.new do |y|
|
12
|
+
begin
|
13
|
+
method = obj.instance_method(name)
|
14
|
+
rescue NameError
|
15
|
+
end
|
16
|
+
|
17
|
+
loop do
|
18
|
+
break if method.nil?
|
19
|
+
|
20
|
+
y << MethodParameters.new(method.parameters)
|
21
|
+
method = method.super_method
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
else
|
26
|
+
# see https://bugs.ruby-lang.org/issues/13973
|
27
|
+
def self.of(obj, name)
|
28
|
+
Enumerator.new do |y|
|
29
|
+
ancestors = obj.ancestors
|
30
|
+
|
31
|
+
loop do
|
32
|
+
klass = ancestors.shift
|
33
|
+
break if klass.nil?
|
34
|
+
|
35
|
+
begin
|
36
|
+
method = klass.instance_method(name)
|
37
|
+
|
38
|
+
next unless method.owner.equal?(klass)
|
39
|
+
rescue NameError
|
40
|
+
next
|
41
|
+
end
|
42
|
+
|
43
|
+
y << MethodParameters.new(method.parameters)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_reader :parameters
|
50
|
+
|
51
|
+
def initialize(parameters)
|
52
|
+
@parameters = parameters
|
53
|
+
end
|
54
|
+
|
55
|
+
def splat?
|
56
|
+
return @splat if defined? @splat
|
57
|
+
@splat = parameters.any? { |type, _| type == :rest }
|
58
|
+
end
|
59
|
+
|
60
|
+
def sequential_arguments?
|
61
|
+
return @sequential_arguments if defined? @sequential_arguments
|
62
|
+
@sequential_arguments = parameters.any? { |type, _|
|
63
|
+
type == :req || type == :opt
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def keyword_names
|
68
|
+
@keyword_names ||= parameters.each_with_object(Set.new) { |(type, name), names|
|
69
|
+
names << name if type == :key || type == :keyreq
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def keyword?(name)
|
74
|
+
keyword_names.include?(name)
|
75
|
+
end
|
76
|
+
|
77
|
+
def empty?
|
78
|
+
parameters.empty?
|
79
|
+
end
|
80
|
+
|
81
|
+
def length
|
82
|
+
parameters.length
|
83
|
+
end
|
84
|
+
|
85
|
+
def pass_through?
|
86
|
+
parameters.eql?(PASS_THROUGH)
|
87
|
+
end
|
88
|
+
|
89
|
+
EMPTY = new([])
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-container'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module AutoInject
|
7
|
+
class Strategies
|
8
|
+
extend Dry::Container::Mixin
|
9
|
+
|
10
|
+
# @api public
|
11
|
+
def self.register_default(name, strategy)
|
12
|
+
register name, strategy
|
13
|
+
register :default, strategy
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'dry/auto_inject/strategies/args'
|
20
|
+
require 'dry/auto_inject/strategies/hash'
|
21
|
+
require 'dry/auto_inject/strategies/kwargs'
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/auto_inject/strategies/constructor'
|
4
|
+
require 'dry/auto_inject/method_parameters'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module AutoInject
|
8
|
+
class Strategies
|
9
|
+
# @api private
|
10
|
+
class Args < Constructor
|
11
|
+
private
|
12
|
+
|
13
|
+
def define_new
|
14
|
+
class_mod.class_exec(container, dependency_map) do |container, dependency_map|
|
15
|
+
define_method :new do |*args|
|
16
|
+
deps = dependency_map.to_h.values.map.with_index { |identifier, i|
|
17
|
+
args[i] || container[identifier]
|
18
|
+
}
|
19
|
+
|
20
|
+
super(*deps, *args[deps.size..-1])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def define_initialize(klass)
|
26
|
+
super_parameters = MethodParameters.of(klass, :initialize).each do |ps|
|
27
|
+
# Look upwards past `def foo(*)` methods until we get an explicit list of parameters
|
28
|
+
break ps unless ps.pass_through?
|
29
|
+
end
|
30
|
+
|
31
|
+
if super_parameters.empty?
|
32
|
+
define_initialize_with_params
|
33
|
+
else
|
34
|
+
define_initialize_with_splat(super_parameters)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def define_initialize_with_params
|
39
|
+
initialize_args = dependency_map.names.join(', ')
|
40
|
+
|
41
|
+
instance_mod.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
42
|
+
def initialize(#{initialize_args})
|
43
|
+
#{dependency_map.names.map { |name| "@#{name} = #{name}" }.join("\n")}
|
44
|
+
super()
|
45
|
+
end
|
46
|
+
RUBY
|
47
|
+
end
|
48
|
+
|
49
|
+
def define_initialize_with_splat(super_parameters)
|
50
|
+
super_pass = if super_parameters.splat?
|
51
|
+
'*args'
|
52
|
+
else
|
53
|
+
"*args.take(#{super_parameters.length})"
|
54
|
+
end
|
55
|
+
|
56
|
+
instance_mod.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
57
|
+
def initialize(*args)
|
58
|
+
#{dependency_map.names.map.with_index { |name, i| "@#{name} = args[#{i}]" }.join("\n")}
|
59
|
+
super(#{super_pass})
|
60
|
+
end
|
61
|
+
RUBY
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
register :args, Args
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/auto_inject/dependency_map'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module AutoInject
|
7
|
+
class Strategies
|
8
|
+
class Constructor < Module
|
9
|
+
ClassMethods = Class.new(Module)
|
10
|
+
InstanceMethods = Class.new(Module)
|
11
|
+
|
12
|
+
attr_reader :container
|
13
|
+
attr_reader :dependency_map
|
14
|
+
attr_reader :instance_mod
|
15
|
+
attr_reader :class_mod
|
16
|
+
|
17
|
+
def initialize(container, *dependency_names)
|
18
|
+
@container = container
|
19
|
+
@dependency_map = DependencyMap.new(*dependency_names)
|
20
|
+
@instance_mod = InstanceMethods.new
|
21
|
+
@class_mod = ClassMethods.new
|
22
|
+
end
|
23
|
+
|
24
|
+
# @api private
|
25
|
+
def included(klass)
|
26
|
+
define_readers
|
27
|
+
|
28
|
+
define_new
|
29
|
+
define_initialize(klass)
|
30
|
+
|
31
|
+
klass.send(:include, instance_mod)
|
32
|
+
klass.extend(class_mod)
|
33
|
+
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def define_readers
|
40
|
+
instance_mod.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
41
|
+
attr_reader #{dependency_map.names.map { |name| ":#{name}" }.join(', ')}
|
42
|
+
RUBY
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def define_new
|
47
|
+
raise NotImplementedError, "must be implemented by a subclass"
|
48
|
+
end
|
49
|
+
|
50
|
+
def define_initialize(klass)
|
51
|
+
raise NotImplementedError, "must be implemented by a subclass"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|