Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
[WIP] Dry::KeyInject, Dry::GetterInject and Dry::SetterInject inspired by https://github.com/dryrb/dry-auto_inject/

Dry::AutoInject Extra Strategies

Dry::KeyInject

Same as Dry::AutoInject but uses keyword arguments. This allows you to replace just one dependency at a time and rest will use the defaults provided by the container.

require 'dry-container'

MEMORY_DB = Hash[messages:[]]

module MyApp
  container = Dry::Container.new
  container.register('message.store', -> { MEMORY_DB[:messages] })
  container.register(:messenger, -> (msg) { puts msg })

  AutoInject = Dry::KeyInject(container)

  def self.Inject(*keys)
    AutoInject[*keys]
  end

  class Service
    include MyApp::Inject('message.store', :messenger)

    def call(params)
      store << params[:msg]
      messenger.(params[:msg])
    end
  end
end

svc = MyApp::Service.new
svc.(msg: "Hello")
# => Hello
puts MEMORY_DB[:messages]
# => Hello
puts [svc.public_methods - Object.new.public_methods].join(', ')
# => call, store, messenger
puts MyApp::Service.container.inspect
# => #<Dry::Container:0x007fc3a3b39c28>
svc = MyApp::Service.new(messenger: -> (msg) { puts "#{msg.to_s.upcase}!" })
svc.(msg: "Hello")
# => HELLO!

Dry::GetterInject

This one is ideal when you have no control over initialization (e.g. Rails controllers). Getter injection simply creates reader accessors with chosen visibility (private/protected/public). Default is public visibility as in original AutoInject strategy. Only way how you can change those dependcies is via the container, which is assigned to the class object or via stubbing in the tests.

require 'dry-container'

MEMORY_DB = Hash[messages:[]]

module MyApp
  container = Dry::Container.new
  container.register('message.store', -> { MEMORY_DB[:messages] })
  container.register(:messenger, -> (msg) { puts msg })

  AutoInject = Dry::GetterInject(container, :private)

  def self.Inject(*keys)
    AutoInject[*keys]
  end

  class Controller
    include MyApp::Inject('message.store', :messenger)

    def initialize(params)
      @params = params
    end

    def show
      store << params[:msg]
      messenger.(params[:msg])
    end

    protected
    attr_reader :params
  end
end

ctrl = MyApp::Controller.new(msg: "Hello")
ctrl.show
# => Hello
puts MEMORY_DB[:messages]
# => Hello
puts ctrl.public_methods - Object.new.public_methods
# => show
puts MyApp::Controller.container.inspect
# => #<Dry::Container:0x007fc3a3b39c28>

Dry::SetterInject

This one is extension of the Dry::GetterInject and it creates both readers (with given visibility) and (always public) writers which allows you to do setter DI. This allows you plugin replacement of any dependency during runtime, which gives you more control and make the given class more reusable. However every post-initialize mutation makes the class less stable so it should be used with care. You should always prefer constructor DI as is implemented in Dry::AutoInject or Dry::KeyInject.

require 'dry-container'

MEMORY_DB = Hash[messages:[]]

module MyApp
  container = Dry::Container.new
  container.register('message.store', -> { MEMORY_DB[:messages] })
  container.register(:messenger, -> (msg) { puts msg })

  AutoInject = Dry::SetterInject(container, :private)

  def self.Inject(*keys)
    AutoInject[*keys]
  end

  class Controller
    include MyApp::Inject('message.store', :messenger)

    def initialize(params)
      @params = params
    end

    def show
      store << params[:msg]
      messenger.(params[:msg])
    end

    protected
    attr_reader :params
  end
end

ctrl = MyApp::Controller.new(msg: "Hello")
ctrl.show
# => Hello
puts MEMORY_DB[:messages]
# => Hello
puts [ctrl.public_methods - Object.new.public_methods].join(', ')
# => show
puts MyApp::Controller.container.inspect
# => #<Dry::Container:0x007fc3a3b39c28>
ctrl.messenger = -> (msg) { puts "#{msg.to_s.upcase}!" }
ctrl.show
# => HELLO!
module Dry
# @api public
def self.GetterInject(container, visibility)
-> *names { GetterInject.new(names, container, visibility) }
end
class GetterInject < Module
attr_reader :names,
:keys,
:visibility,
:container,
:instance_mod
# @api private
def initialize(keys, container, visibility)
@keys = keys
@names = keys.map(&:to_s).map { |s| s.split('.').last }.map(&:to_sym)
@visibility = visibility
@container = container
@instance_mod = Module.new
end
# @api private
def included(klass)
define_container(klass)
define_accessors(names, keys, visibility)
klass.send(:include, instance_mod)
super
end
private
# @api private
def define_container(klass)
klass.instance_variable_set('@container', container)
klass.class_eval do
def self.container
if superclass.respond_to?(:container)
superclass.container
else
@container
end
end
end
end
# @api private
def define_accessors(names, keys, visibility)
instance_mod.module_eval do
names.each.with_index do |name, i|
define_method(name) do
self.class.container[keys[i]]
end
end
case visibility
when :private then private *names
when :protected then protected *names
end
end
end
end
end
module Dry
# @api public
def self.KeyInject(container)
-> *names { KeyInject.new(names, container) }
end
# @api private
class KeyInject < Module
attr_reader :names
attr_reader :container
attr_reader :instance_mod
attr_reader :ivars
attr_reader :map
# @api private
def initialize(names, container)
@names = names
@container = container
@ivars = names.map(&:to_s).map { |s| s.split('.').last }.map(&:to_sym)
@instance_mod = Module.new
define_constructor
end
# @api private
def included(klass)
define_new_method(klass)
define_container(klass)
klass.send(:include, instance_mod)
super
end
private
# @api private
def define_container(klass)
klass.instance_variable_set('@container', container)
klass.class_eval do
def self.container
if superclass.respond_to?(:container)
superclass.container
else
@container
end
end
end
end
# @api private
def define_new_method(klass)
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
def self.new(**kargs)
keys = %i[#{ivars.map(&:to_s).join(' ')}]
names = [#{names.map(&:inspect).join(', ')}]
deps = names.map.with_index { |n, i| kargs[n] || container[names[i]] }
kwargs = keys.zip(deps).to_h
super(**kwargs)
end
RUBY
end
# @api private
def define_constructor
instance_mod.class_eval <<-RUBY, __FILE__, __LINE__ + 1
attr_reader #{ivars.map { |name| ":#{name}" }.join(', ')}
def initialize(**kargs)
super()
#{ivars.map { |name| "@#{name} = kargs[:#{name}]" }.join("\n")}
end
RUBY
self
end
end
end
module Dry
# @api public
def self.SetterInject(container, visibility)
-> *names { SetterInject.new(names, container, visibility) }
end
class SetterInject < Module
attr_reader :names,
:keys,
:visibility,
:container,
:instance_mod
# @api private
def initialize(keys, container, visibility)
@keys = keys
@names = keys.map(&:to_s).map { |s| s.split('.').last }.map(&:to_sym)
@visibility = visibility
@container = container
@instance_mod = Module.new
end
# @api private
def included(klass)
define_container(klass)
define_writers(names, keys)
define_readers(names, keys, visibility)
klass.send(:include, instance_mod)
super
end
private
# @api private
def define_container(klass)
klass.instance_variable_set('@container', container)
klass.class_eval do
def self.container
if superclass.respond_to?(:container)
superclass.container
else
@container
end
end
end
end
# @api private
def define_writers(names, keys)
instance_mod.module_eval do
names.each.with_index do |name, i|
define_method("#{name}=") do |value|
instance_variable_set("@#{name}", value)
end
end
end
end
# @api private
def define_readers(names, keys, visibility)
instance_mod.module_eval do
names.each.with_index do |name, i|
define_method(name) do
instance_variable_get("@#{name}") || self.class.container[keys[i]]
end
end
case visibility
when :private then private *names
when :protected then protected *names
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.