Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass keyword dependencies if super constructor accepts arguments with the splat operator #48

Merged
merged 8 commits into from
Nov 29, 2018

Conversation

flash-gordon
Copy link
Member

@flash-gordon flash-gordon commented Nov 25, 2018

Now the keyword strategy will pass dependencies to an existing initialize method if it accepts an arbitrary number of arguments. This enables seamless integration with ROM's repositories. The required changes in ROM have been applied already so we're going to have it in ROM 5.

Resolves #46, Resolves #49

@flash-gordon
Copy link
Member Author

@timriley surprisingly, it works :) I created a dedicated class for handling method parameters so it's easier to track intentions throughout the code

@flash-gordon
Copy link
Member Author

Hah, aaaand I was wrong. I have rails controllers with injections and this doesn't work anymore. The case looks like this

module SomeMixin
  def initialize(*)
    super
  end
end

class BaseClass
  def initialize
    #
  end
end

class ChildClass < BaseClass
  include SomeMixin
end

class MyAppClass < ChildClass
  include ImportModule['dependency']
end

Personally, I don't care, I can easily work around it with adding a mixin to ChildClass, i.e.

ChildClass.include Module.new { def initialize; super end }

However, this means the change is not backward compatible.

Copy link
Member

@timriley timriley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@flash-gordon This is good stuff, thanks for putting the time and thought into it!

I'm looking at that rails controller meta-example of yours and trying to work out exactly why it's failing... is it because it ends up trying to pass the dependencies upwards into a place where they're not wanted/expected (due to the SomeMixin#initialize(*))? I'm interested, which #initialize params signature in the chain actually causes things to break?

Either way, if this is due to Rails controllers working in some non-conventional way, I agree: I don't think we should bend over backwards to support them. If it turned out lots of people wanted easier support at some case, we could ship a special strategy in a companion gem or something.

Left a couple of code questions for you, otherwise.

super()
end
RUBY
instance_mod.class_exec(dependency_map) do |dependency_map|
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually need to pass dependency_map into class_exec and its block if we're not explicitly using it there? With this new factoring it seems we're using it via the assign_dependencies and slice_kwargs methods made available to that block through regular closure behaviour?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch, fixed

end
RUBY
instance_mod.class_exec(dependency_map) do |dependency_map|
define_method :initialize do |**kwargs|
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So previously here we were building an initialize method with a full set of named keyword params, and now we're just accepting a kwsplat. I just wanted to check, @flash-gordon, was this change just so we could reuse the assign_dependencies/slice_kwargs helpers across both initialise-builders here, or was there some other reason specifically for making this version of #define_initialize_with_keywords work?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exactly this, otherwise we would come up with two define_initialize_with_keywords with unclear differences between them. Now we at least have east-to-grasp logic there

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I think clarity of internal implementation is probably the better thing to optimise for here 👍

@@ -26,6 +26,9 @@ def self.remove_constants
end

RSpec.configure do |config|
config.disable_monkey_patching!
config.filter_run_when_matching :focus
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually caused an error for me:

undefined method `filter_run_when_matching' for #<RSpec::Core::Configuration:0x00007fe0062dfb48> (NoMethodError)
Did you mean?  filter_run_excluding

Perhaps we should specify a particular version of rspec in our Gemfile?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's surprising. Fixed this by raising the rspec version in gemspec.


it "passes dependencies assuming the parent class can take them" do
instance = child_class.new

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we could add this to make it clear the standard behaviour is still in place?

Suggested change
expect(instance.one).to eq "dep 1"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

def self.super_method(klass, method)
method = klass.instance_method(method)
method unless method.owner.equal?(klass)
def self.super_parameters(klass, method_name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this! (and MethodParameters too!)

@flash-gordon
Copy link
Member Author

@timriley the reason it failed is the mixin is a url helper, it throws in some instance variables in the constructor and doesn't really care about its arguments, hence (*) + super. auto_inject sees splat, decides to pass the deps further, to the helper's constructor. When the helper calls super it actually delegates to def initialize() in the base class but it has 0 arity and thus fails.

I think it's a valid use case but at the same it's quite easy to work around the issue with having

include Module.new {
  def initialize(*)
    super()
  end
}

in ApplicationController.

@timriley
Copy link
Member

timriley commented Nov 27, 2018

@flash-gordon I wonder if we could actually fix this by making the opening of Kwarg's #define_initialze follow the same approach as the Args strategy? i.e.

super_parameters = Dry::AutoInject.super_parameters(klass, :initialize).each do |ps|
  # Look upwards past `def foo(*)` methods until we get an explicit list of parameters
  break ps unless ps.pass_through?
end

Wouldn't that allow us to see through your mixin's initializer to the one with 0 arity and then decide to pass nothing up to it?

@flash-gordon
Copy link
Member Author

@timriley that's a very good point, I'll try it out

This also

* bumps the minimal required ruby version to 2.2.0 because of using
  Method#super_method
* fixes the behavior of AutoInject.super_parameters (renamed to
  MethodParameters.of), it didn't work properly. I think, I unlearned
  how to write loops :(
* adds specs for MethodParameters.of
@flash-gordon
Copy link
Member Author

@timriley I made it work and fixed some bugs along the way c20c0f9

@flash-gordon
Copy link
Member Author

build fails on 2.4.3 but works on 2.4.5 🤔🤔🤔

@flash-gordon
Copy link
Member Author

https://bugs.ruby-lang.org/issues/13973 the fix is in ruby 2.4.4 and above, I'll make a workaround for lower versions

@flash-gordon
Copy link
Member Author

done, this somehow fixed jruby as well 🤷‍♀️

This improves compatibility when e.g. test doubles are passed as dependencies, which in the context of the test may not appreciate having extra methods like `#nil?` called on them.
@timriley
Copy link
Member

@flash-gordon Have tested this in my current app here and it's working great.

I hope you don't mind, but I've just pushed a couple of extra commits here which will allow this branch to properly close off a few bug reports.

6d82cf8 adds test to demonstrate we resolve #46 as part of your changes here.

a49c1cc tweaks the conditional assignment of deps just a little (made possible now we're just taking a **kwargs for all our constructors) so we can avoid calling #nil? on the dep objects themselves, which will resolve #49.

@timriley
Copy link
Member

Have updated the description to note those issue resolutions there too. Happy for you to merge this, @flash-gordon! Thanks! 🎉

@flash-gordon flash-gordon merged commit 8e7ce83 into master Nov 29, 2018
@flash-gordon flash-gordon deleted the passing-deps-when-splat-present branch November 29, 2018 07:16
@solnic
Copy link
Member

solnic commented Nov 29, 2018

This is outstanding work @flash-gordon <3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants