dry-rb / dry-auto_inject

Container-agnostic constructor injection mixin

Home Page:https://dry-rb.org/gems/dry-auto_inject/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Error when a parent class recieves arbitrary number of arguments and a parent of the parent is not

flash-gordon opened this issue · comments

It's better to show on modules actually:

module Shiny
  def initialize(*)
    super
    @foo = 1
  end
end

Inject = Dry::AutoInject(one: 1)

class Foo
  include Shiny
  include Inject[:one]
end

Foo.new

__END__
[8] pry(main)> wtf?
Exception: ArgumentError: wrong number of arguments (given 1, expected 0)
--
0: (pry):3:in `initialize'
1: (pry):3:in `initialize'
2: /Users/gordon/dev/dry-auto_inject/lib/dry/auto_inject/injection.rb:123:in `initialize'

@timriley do you think the case is valid? I think about removing all injected arguments prior to call super method as a default behavior, what do you think?

Oh, and one more issue. Currently .new accepts arbitrary number of arguments but silently ignores superfluous.

@flash-gordon Very interesting! I was about to say that this is probably not about, that Shiny#initialize should have some understanding of its circumstances and pass the appropriate args to super (or no args, via super()).

However, I did notice that Shiny#initialize's parameters is different to what I've been checking for so far. It returns [[:rest]], instead of [[:rest, :args]], which is what you get for a method that accepts a named splat argument like *args.

When there's no name for the splat, then it's clear the method has no intention to use them, so I think we should probably handle this case specially. When we know the super #initialize has * args, then we should probably just pass none of the dependencies to it, the same as this case. What do you think?

Yes, I thought about it and it does the job in that case, we can be sure that Shiny won't do super() because it makes no sense as far as I can imagine. But the issue can be also resolved with something like this:

@@ -67,7 +67,7 @@ module Dry
           def self.new(*args)
             names = #{dependency_map.inspect}
             deps = names.values.map.with_index { |identifier, i| args[i] || container[identifier] }
-            super(*deps)
+            super(*deps, *args[deps.size..-1])
           end
         RUBY
       end
@@ -110,12 +110,10 @@ module Dry
       # @api private
       def define_constructor_with_args(klass)
         super_method = Dry::AutoInject.super_method(klass, :initialize)
-        super_params = if super_method.nil? || super_method.parameters.empty?
+        super_params = if super_method.nil?
           ''
-        elsif super_method.parameters.any? { |type, _| type == :rest }
-          '*args'
         else
-          "*args[0..#{super_method.parameters.length - 1}]"
+          "*args[#{dependency_map.size}..-1]"
         end

I suggest it because I see no reason why parent class should know about dependencies of child classes. Could you explain the reason to me because auto-inject is very awesome and I want to use it properly :)

It's worth to mention about one more case:

[1] pry(main)> Inject = Dry::AutoInject(one: 1, two: 2)
=> #<Dry::AutoInject::Injector:0x007f999281eda8 @container={:one=>1, :two=>2}, @options={}>
[2] pry(main)> class Foo
[2] pry(main)*   include Inject[:one]
[2] pry(main)* end
=> Foo
[3] pry(main)> class Bar < Foo
[3] pry(main)*   include Inject[:two]
[3] pry(main)* end
=> Bar
[4] pry(main)> Bar.new
=> #<Bar:0x007f9993321de0 @one=2, @two=2> 
[5] pry(main)>

:)

BTW it came to my mind that we can provide different strategies for sequential parameters.