-
Notifications
You must be signed in to change notification settings - Fork 897
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
[WIP] refactor SupportsFeatureMixin to do less metaprogramming #11322
Conversation
Can you show an example of what this actually solves? ActiveSupport::Concern is already at this modules level and should alleviate your problem unless I'm misunderstanding. |
Is this even possible? Again, what is this trying to solve... [1] pry(main)> class Foo < Object; end
=> nil
[2] pry(main)> module MyMixin; end
=> nil
[3] pry(main)> Foo.include(MyMixin)
=> Foo
[4] pry(main)> Foo.ancestors
=> [Foo, MyMixin, Object, PP::ObjectMixin, Kernel, BasicObject]
[5] pry(main)> Foo.include(MyMixin)
=> Foo
[6] pry(main)> Foo.ancestors
=> [Foo, MyMixin, Object, PP::ObjectMixin, Kernel, BasicObject]
[7] pry(main)> module AnotherMixin; include MyMixin; end
=> AnotherMixin
[8] pry(main)> Foo.include(AnotherMixin)
=> Foo
[9] pry(main)> Foo.ancestors
=> [Foo, AnotherMixin, MyMixin, Object, PP::ObjectMixin, Kernel, BasicObject] Notice it's not there twice at the bottom (line 9) nor in line 6 |
If you do need to include it, perhaps instead of blowing up, the |
I think part of the oddity is the creation of methods being inside included which isn't "normal" mixin behavior. I wonder if it's better to just have the methods defined directly in the SupportsFeatureMixin, then mixed in naturally, instead of dynamically creating them into the includee. module SupportsFeatureMixin
def supports_not; end
included do
QUERYABLE_FEATURES.keys.each do |feature|
supports_not(feature)
end
end
end
# instead maybe
module SupportsFeatureMixin
def supports_not(*args); end
QUERYABLE_FEATURES.keys.each do |feature|
supports_not(feature)
end
end |
Copied from Gitter to demonstrate "normal" inclusion of methods vs dynamically defining methods in the included callback. [1] pry(main)> module MyMixin; def foo; end; end
=> :foo
[2] pry(main)> class MyClass; include MyMixin; end
=> MyClass
[3] pry(main)> MyClass.instance_method(:foo)
=> #<UnboundMethod: MyClass(MyMixin)#foo>
[4] pry(main)> module MyMixin2; def self.included(other); other.class_eval { def foo; end }; end; end
=> :included
[5] pry(main)> class MyClass2; include MyMixin2; end
=> MyClass2
[6] pry(main)> MyClass2.instance_method(:foo)
=> #<UnboundMethod: MyClass2#foo> Might be a little hard to see but the first 3 lines are "normal" includes...notice the method lives in MyMixin. The last 3 lines are when you dynamically define a method...notice the method lives in MyClass2. Now, if you created a method foo in MyClass it would "override" the one in MyMixin because of the class ancestry. If you create a method foo in MyClass2, it may or may not override the mixed in one depending on the order (and in fact you lose the old one and can't even call super anymore). |
@@ -74,6 +74,14 @@ module SupportsFeatureMixin | |||
# Whenever this mixin is included we define all features as unsupported by default. | |||
# This way we can query for every feature | |||
included do | |||
if self.class == Module | |||
raise "SupportsFeatureMixin cannot be included into modules. You have to use 'extend ActiveSupport::Concern'" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have to use 'extend ActiveSupport::Concern'
Huh? 😕
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😄 yea, ack this reads awkward, but I couldnt come up with a better oneliner to explain this documentation
But maybe this whole PR isnt needed at all because I suck at inclusion
1a865d2
to
0cc8399
Compare
The first case in the original description is still true. class A
module SubModule
def honk
"overwrites with A::SubModule"
end
end
def honk
"A"
end
include SubModule
end
p A.new.honk
# "A"
p A.ancestors
# [A, A::SubModule, Object, Kernel, BasicObject]
class B < A
module SubModule
def honk
"overwrites with SubModule B::SubModule"
end
end
include SubModule
end
p B.new.honk
# "overwrites with SubModule B::SubModule"
p B.ancestors
# [B, B::SubModule, A, A::SubModule, Object, Kernel, BasicObject] Now, part of this stems from making every feature unsupported by default upon inclusion of Therefore I propose raising an error, as in the previous commit 0cc8399, in case the Mixin is included into a module, which is not the intended usage. |
So your example is overwriting like that because A::SubModule is a completely different module from B::SubModule. That is, you are no re-opening A::SubModule but instead defining a new one. Even so, I'm not sure how your example is analogous to SupportsFeatureMixin |
Its analogous to our layout of modules that get included by So, I'd ditch my last commit to this PR and would like to have this commit 0cc8399 merged |
updated OP to be clearer |
1b80d29
to
6c68d8f
Compare
6c68d8f
to
5b79a01
Compare
@Fryguy @chrisarcand please have a look at the updated description. |
5b79a01
to
e29c715
Compare
@chrisarcand could you have a look, please? |
method_name = "supports_#{feature}?" | ||
|
||
# defines the method on the instance | ||
define_method(method_name) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don't think this is necessary here...you should be able to build all this into the base part of the mixin, and then they will be included automatically (and in fact, will be properly defaulted.
unsupported[feature] | ||
end | ||
|
||
# query the instance if the feature is supported or not | ||
def supports?(feature) | ||
SupportsFeatureMixin.guard_queryable_feature(feature) | ||
public_send("supports_#{feature}?") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we even need the meta methods anymore? supports?(:feature)
seems perfectly fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly my thought as well. I'd really rather just keep it to a parameterized API if possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, thought so too.
but I wanted to do it in a follow up, to focus this PR on the groundworks
end | ||
!unsupported.key?(feature) | ||
end | ||
private |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wonder why rubocop is complaining about this private (perhaps you have a second one somewhere earlier)?
@jrafanie Check it out...code climate engines to the rescue :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is rubocop complaining? these are private class methods
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, now I 👀 Its codeclimate bugging us
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, codeclimate :)
When I read this the first thing I think is that you shouldn't need to traverse this yourself. Ruby's super can handle that for you. I love the idea of having class-level overrides. So, pesudo-code-like I'm thinking class BaseClass
supports_not :foo # default values all at the base class that setup the class-level definitions
supports_not :bar
def self.supports?(feature) # Mixed in from SupportsFeatureMixin
supports_overridden_at_this_class_level?(feature) : supports_override_value(feature) : super
end
end
class SubClass < BaseClass
supports :foo
end Then calling SubClass.supports?(:foo) would check the override at that level and return true, but calling SubClass.supports?(:bar) would not see a override at that level, and then would call super. super will go up to the BaseClass where all of the supports_not are defined, and you'd get false. |
I dont think I can do this :( The override via But I can try to investigate that (again) in a follow up PR |
@durandom Thanks so much for your patience as I've had little time to look at this until now, not to mention catching up on all the conversation already had about this. Lots of thoughts, but a few main ones:
I'd like to continue on with this and wait on merging this for a bit longer; let's definitely have some solution (this one or another) in place and merged this week for you though. |
This is why I came up with |
I think there's some confusion here that might be clearer when I have an example for you, if it works. Forthcoming! |
Opened #12227 to fix the core of what's trying to be fixed here (with simple inheritance), though the spec included in this PR does not pass (expecting the class methods to be overridden via a simple |
end | ||
end | ||
|
||
class UnknownFeatureError < StandardError; end | ||
|
||
class FeatureDefinition | ||
def initialize(supported: false, block: nil, unsupported_reason: nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to just use a struct to simplify this code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes. actually it started out as a struct, got more complicated and became a class. Now it could be a struct again 😄
But in a follow up PR I'm adding support for class level blocks, so this can get a bit more complex again.
If you dont mind, I'd leave it as a class
now features are defined internally in a FeatureDefinition class. An instance of this is added to the module or class upon using the supports and supports_not DSL.
e29c715
to
e812f50
Compare
Checked commits durandom/manageiq@e812f50~...349e048 with ruby 2.2.5, rubocop 0.37.2, and haml-lint 0.16.1 app/models/mixins/supports_feature_mixin.rb
|
Just got that on gitter. Another reason to remove dynamically defined methods (which I introduced, so blame me :) |
<pr_mergeability_checker />This pull request is not mergeable. Please rebase and repush. |
@miq-bot add_label wip I still think this is a relevant and needed refactoring |
This pull request has been automatically closed because it has not been updated for at least 6 months. Feel free to reopen this pull request if these changes are still valid. Thank you for all your contributions! |
Before SupportsFeature should not be included in modules because methods get created on inclusion time.
I refactored the Mixin to not generate the
supports_feature?
methods upon calling the DSLsupports :feature
methods. The reason of doing that was to leverage rubys method lookupchain and have subclasses override default behavior of base classes.Now I store a
FeatureDefinition
into a class or module variablesupported_feature_definitions
upon calling the DSL. When asking an objectsupports?(:feature)
I traverse theancestors
to find the firstFeatureDefinition
and use that to determine if the feature is supported or not.While both solutions look similar, the first one fails when
SupportsFeatureMixin
is included into a Module and the DSL methods are called on the module (like in the example above). This created methods which would hide other methods (because every feature would be unsupported by default).I think this approach makes it easier to use, because now you can use the
included
block andActiveSupport::Concern
to forward the DSL to the included module. But you dont have to.