Dry-AutoInject domain specification

Hello!

I had an idea on how to add semantics to the importer.

When I want to import something on my code-base I have to do something like this :

include AppName::Import[servers: 'domain.organization.server_repository']

This allows too easily to access objects from another domain.
I just have to change the organization by something else to access the server_repository of my domain.

I had the idea to namespace the import with a proxy :

# frozen_string_literal: true

class InjectorNamespaced
  # @param [Dry::AutoInject::Builder] builder
  # @param [String] default_namespace
  def initialize(builder, default_namespace)
    @builder = builder
    @default_namespace = default_namespace
  end

  def [](*dependency_names)
    dependency_names
      .map { |dependency| handle_dependency(dependency) }
      .then { |names| @builder[*names] }
  end

  # rubocop:disable Style/OptionalBooleanParameter
  def respond_to?(name, include_private = false)
    @builder.respond_to?(name, include_private)
  end
  # rubocop:enable Style/OptionalBooleanParameter

  private

  def handle_dependency(dependency)
    case dependency
    when String
      @default_namespace + dependency
    when Hash
      dependency.transform_values { |name| @default_namespace + name }
    else
      raise :invalid_type
    end
  end
end

Thank this, I can specify on with domain my object belongs.

I define an importer object

# frozen_string_literal: true

require_relative 'lib/injector_namespaced'

module Importers
  Application = InjectorNamespaced.new(AppName::Import, 'application.')
  Infrastructure = InjectorNamespaced.new(AppName::Import, 'infrastructure.')

  module Domain
    Organization = InjectorNamespaced.new(AppName::Import, 'domain.organization.')
  end
end

And to simplify usage I alias them

module Domain
  module Organization
    Importer = Importers::Domain::Organization
  end
end

Get back to the initial line, I now can do

include Domain::Organization::Importer[servers: 'server_repository']

This enforces the fact that this importer can only load an object from the domain organization.

As my class is inside the module Domain::Organization I can omit this information

include Importer[servers: 'server_repository']
2 Likes

Also, by adding support to inject by class constants would improve IDE code navigation… 100% agree with this idea

Yes this is something we’ve been considering for a while now. We can revisit this idea after dry-system 1.0.0 / Hanami 2.0 are released.

@drqCode not sure if I follow - it doesn’t use class constants. When it comes to IDE/editor integration, we need dedicated plugins. It’s funny how it never bothered me because I open files via quick-find with fuzzy searching but I know it is a serious issue for many folks. I wonder if we could build an extension for something like solargraph that would provide various nice features on top of dry-system API. There’s a lot that can be done to provide great developer experience.

1 Like

@solnic Given the above exemple:

module Domain
  module Organization
    class CreateServer
        # the importer transforms the constant to the underscore_case form
        include Importer[servers: ServerRepository] 
     
      
      ...
    end
  end
end

Obviously, this will work only for auto registered objects (the constants should exist btw). Not sure if this creates more issues than benefits though…

This would defeat the purpose of using auto-injection as it would tightly couple your CreateServer class with ServerRepository class.

Well, in my case it’s not so tighly coupled, since I can still register another instance at that key, but with the existence of ServerRepository required. In my most cases, registered components are sigletons. Imo, such support will not defeat the purpose of autoinject, since it just transorms the class name to the corresponding key)

That’s what I meant, it’s tightly coupled to the class because it refers to it explicitly so you need to ensure that it’s always defined.

The goal of DI is not to have a 1:1 coupling between your class and your injections.

For example inside the repository, I include a container, which allows me to switch between the legacy DB and the new DB in the matter of one line.
Useful for a migration project, and awful to do in ActiveRecord.

Specifying the class name would say to your class users that it’s the class you want, not the concept.

I would love for example to add a “Contract” to my injections but my unit tests make sure it’s ok.
Maybe Athena in Crystal could do that, but it’s not Ruby. :slight_smile: