-
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -102,38 +102,77 @@ module SupportsFeatureMixin | |
:update_network_router => 'Network Router Update', | ||
}.freeze | ||
|
||
# Whenever this mixin is included we define all features as unsupported by default. | ||
# This way we can query for every feature | ||
included do | ||
QUERYABLE_FEATURES.keys.each do |feature| | ||
supports_not(feature) | ||
method_name = "supports_#{feature}?" | ||
|
||
# defines the method on the instance | ||
define_method(method_name) do | ||
supports?(feature) | ||
end | ||
|
||
# defines the method on the class | ||
define_singleton_method(method_name) do | ||
supports?(feature) | ||
end | ||
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 commentThe 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 commentThe 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 😄 If you dont mind, I'd leave it as a class |
||
@supported = supported | ||
@block = block | ||
@unsupported_reason = unsupported_reason | ||
end | ||
|
||
def supported? | ||
@supported | ||
end | ||
|
||
def unsupported_reason | ||
SupportsFeatureMixin.reason_or_default(@unsupported_reason) unless supported? | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we even need the meta methods anymore? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. yeah, thought so too. |
||
|
||
def block | ||
@block | ||
end | ||
end | ||
|
||
def self.guard_queryable_feature(feature) | ||
unless QUERYABLE_FEATURES.key?(feature.to_sym) | ||
raise UnknownFeatureError, "Feature ':#{feature}' is unknown to SupportsFeatureMixin." | ||
end | ||
end | ||
|
||
def self.reason_or_default(reason) | ||
def self.reason_or_default(reason = nil) | ||
reason.present? ? reason : _("Feature not available/supported") | ||
end | ||
|
||
# query instance for the reason why the feature is unsupported | ||
def unsupported_reason(feature) | ||
SupportsFeatureMixin.guard_queryable_feature(feature) | ||
feature = feature.to_sym | ||
public_send("supports_#{feature}?") unless unsupported.key?(feature) | ||
supports?(feature) unless unsupported.key?(feature) | ||
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}?") | ||
|
||
feature = feature.to_sym | ||
feature_definition = self.class.feature_definition_for(feature) | ||
if feature_definition.supported? | ||
unsupported.delete(feature) | ||
if feature_definition.block | ||
instance_eval(&feature_definition.block) | ||
end | ||
else | ||
unsupported_reason_add(feature, feature_definition.unsupported_reason) | ||
end | ||
!unsupported.key?(feature) | ||
end | ||
|
||
# query the instance if a feature is generally known | ||
|
@@ -159,72 +198,50 @@ def unsupported | |
# This is the DSL used a class level to define what is supported | ||
def supports(feature, &block) | ||
SupportsFeatureMixin.guard_queryable_feature(feature) | ||
define_supports_feature_methods(feature, &block) | ||
supported_feature_definitions[feature.to_sym] = FeatureDefinition.new(supported: true, block: block) | ||
end | ||
|
||
# supports_not does not take a block, because its never supported | ||
# and not conditionally supported | ||
def supports_not(feature, reason: nil) | ||
SupportsFeatureMixin.guard_queryable_feature(feature) | ||
define_supports_feature_methods(feature, :is_supported => false, :reason => reason) | ||
supported_feature_definitions[feature.to_sym] = FeatureDefinition.new(unsupported_reason: reason) | ||
end | ||
|
||
# query the class if the feature is supported or not | ||
def supports?(feature) | ||
SupportsFeatureMixin.guard_queryable_feature(feature) | ||
public_send("supports_#{feature}?") | ||
feature_definition_for(feature).supported? | ||
end | ||
|
||
# query the class for the reason why something is unsupported | ||
def unsupported_reason(feature) | ||
SupportsFeatureMixin.guard_queryable_feature(feature) | ||
feature = feature.to_sym | ||
public_send("supports_#{feature}?") unless unsupported.key?(feature) | ||
unsupported[feature] | ||
feature_definition_for(feature).unsupported_reason | ||
end | ||
|
||
# query the class if a feature is generally known | ||
def feature_known?(feature) | ||
SupportsFeatureMixin::QUERYABLE_FEATURES.key?(feature.to_sym) | ||
end | ||
|
||
private | ||
|
||
def unsupported | ||
# This is a class variable and it might be modified during runtime | ||
# because we dont eager load all classes at boot time, so it needs to be thread safe | ||
@unsupported ||= Concurrent::Hash.new | ||
end | ||
|
||
# use this for making a class not support a feature | ||
def unsupported_reason_add(feature, reason = nil) | ||
SupportsFeatureMixin.guard_queryable_feature(feature) | ||
def feature_definition_for(feature) | ||
feature = feature.to_sym | ||
unsupported[feature] = SupportsFeatureMixin.reason_or_default(reason) | ||
feature_definition = nil | ||
ancestors.detect do |ancestor| | ||
feature_definition = ancestor.try(:supported_feature_definition, feature) | ||
end | ||
feature_definition || FeatureDefinition.new | ||
end | ||
|
||
def define_supports_feature_methods(feature, is_supported: true, reason: nil, &block) | ||
method_name = "supports_#{feature}?" | ||
feature = feature.to_sym | ||
def supported_feature_definition(feature) | ||
supported_feature_definitions[feature] | ||
end | ||
|
||
# defines the method on the instance | ||
define_method(method_name) do | ||
unsupported.delete(feature) | ||
if block_given? | ||
instance_eval(&block) | ||
else | ||
unsupported_reason_add(feature, reason) unless is_supported | ||
end | ||
!unsupported.key?(feature) | ||
end | ||
private | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, codeclimate :) |
||
|
||
# defines the method on the class | ||
define_singleton_method(method_name) do | ||
unsupported.delete(feature) | ||
# TODO: durandom - make reason evaluate in class context, to e.g. include the name of a subclass (.to_proc?) | ||
unsupported_reason_add(feature, reason) unless is_supported | ||
!unsupported.key?(feature) | ||
end | ||
def supported_feature_definitions | ||
@supported_feature_definitions ||= Concurrent::Hash.new | ||
end | ||
end | ||
end |
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.