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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +12 -0
  3. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  4. data/.github/ISSUE_TEMPLATE/---bug-report.md +30 -0
  5. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  6. data/.github/workflows/ci.yml +76 -0
  7. data/.github/workflows/docsite.yml +34 -0
  8. data/.github/workflows/sync_configs.yml +34 -0
  9. data/.gitignore +10 -0
  10. data/.rspec +4 -0
  11. data/.rubocop.yml +95 -0
  12. data/.rubocop_todo.yml +6 -0
  13. data/CHANGELOG.md +257 -0
  14. data/CODE_OF_CONDUCT.md +13 -0
  15. data/CONTRIBUTING.md +29 -0
  16. data/Gemfile +17 -0
  17. data/LICENSE +20 -0
  18. data/README.md +58 -0
  19. data/Rakefile +14 -0
  20. data/bin/console +11 -0
  21. data/bin/setup +7 -0
  22. data/docsite/source/basic-usage.html.md +104 -0
  23. data/docsite/source/how-does-it-work.html.md +45 -0
  24. data/docsite/source/index.html.md +53 -0
  25. data/docsite/source/injection-strategies.html.md +94 -0
  26. data/dry-auto_inject.gemspec +29 -0
  27. data/lib/dry-auto_inject.rb +3 -0
  28. data/lib/dry/auto_inject.rb +46 -0
  29. data/lib/dry/auto_inject/builder.rb +40 -0
  30. data/lib/dry/auto_inject/dependency_map.rb +55 -0
  31. data/lib/dry/auto_inject/injector.rb +39 -0
  32. data/lib/dry/auto_inject/method_parameters.rb +92 -0
  33. data/lib/dry/auto_inject/strategies.rb +21 -0
  34. data/lib/dry/auto_inject/strategies/args.rb +68 -0
  35. data/lib/dry/auto_inject/strategies/constructor.rb +56 -0
  36. data/lib/dry/auto_inject/strategies/hash.rb +41 -0
  37. data/lib/dry/auto_inject/strategies/kwargs.rb +105 -0
  38. data/lib/dry/auto_inject/version.rb +7 -0
  39. data/rakelib/rubocop.rake +20 -0
  40. 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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/auto_inject'
@@ -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