dry-auto_inject 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|