dry-auto_inject 0.7.0

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