dry auto inject initialization without inheritance

Hi Folks, i ran into something today with the Injector that I didn’t have a good solve for right away, so I thought I’d ask. Scenario: we need to initialize a class with a unique property that changes based on context.

All examples are using Ruby 2.7.

class Container
  register :client do
    Service::Client.new
  end
end

Import = Dry::AutoInject(Container)

class Action
  include import[:client]

  def call(data)
    # the action instance needs to pass on some specific config
    # known by the context of the caller
    client.call(config, data)
  end
end

Our current container uses the default kwyd arguments injection strategy, but I couldn’t seem to get around ruby syntax violations. For example:

class Client
end

class Container
  extend Dry::Container::Mixin
  register(:client) do
    Client.new
  end
end

Import = Dry::AutoInject(Container)

class Action
  include Import[:client]

  def initialize(config:); end
end

action = Action.new(config: {})

raises:

in `initialize': unknown keyword: :client (ArgumentError)

and since Action does not inherit, I can’t use the keyword in the subclass, filter it out, and pass it up with super (which I’ve done in subclass cases historically).

thusfar my two best options are of the variety:

class Action
  include Import[:client]

  attr_accessor :config
end

config = {}
action.new.tap { _1.config = config }

any other thoughts on initialization strategies?

Thanks in advance!

Hey Jed. I’m not sure I fully follow what you are trying to do here regarding the Client but, from the examples you’ve provided, I think the piece you are missing is ensuring Action gets constructed with both your config keyword argument and your injected client dependency by passing your dependencies to super.

I rewrote your code snippets, above, as a runable snippet. Note that I wrote this in Ruby 3.1.x syntax to save on vertical space. Regardless, I think all you’ll need to study is how I setup the Action constructor.

Here’s the solution:

#! /usr/bin/env ruby
# frozen_string_literal: true

# Save as `snippet`, then `chmod 755 snippet`, and run as `./snippet`.

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "amazing_print"
  gem "debug"
  gem "dry-container"
  gem "dry-auto_inject"
end

class Client
  def call(config, data) = ap "I'm the client and I just got: Config: #{config}, Data: #{data}."
end

class Container
  extend Dry::Container::Mixin

  register(:client) { Client.new }
end

Import = Dry::AutoInject Container

class Action
  include Import[:client]

  def initialize config:, **dependencies  # Important, so AutoInject and still provide client as a keyword.
    super(**dependencies)
    @config = config
  end

  def call(data) = client.call config, data

  private

  attr_reader :config
end

Action.new(config: {a: 1}).call "test_data"

Running the above will output as:

Fetching gem metadata from https://rubygems.org/...
Resolving dependencies...
"I'm the client and I just got: Config: {:a=>1}, Data: test_data."

Does the above fix your issue?

@bkuhlmann Yes, I think this will solve the issue! My mistake was not destructuring the arguments in the init. Thanks for the quick reply! Jed

1 Like

Just an update here, we were able to refactor our code to use this solution and it works “as advertised”. Thanks for the help!

1 Like