-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Add explicit serializer not found policy #1949
Add explicit serializer not found policy #1949
Conversation
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.
Looks like a good start, just a few comments / questions.
lib/active_model/serializer.rb
Outdated
|
||
if serializer_class | ||
serializer_class | ||
elsif klass.superclass && ![BasicObject, Object].include?(klass.superclass) |
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.
why'd you add to the condition here?
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 wanted to get a meaningful error message. Otherwise, we always get klass == BasicObject
and hence ("Serializer for [BasicObject] not found"), since it's at the bottom of the object hierarchy.
I agree this is a bit wonky, but I couldn't think of anything better.
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.
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.
@NullVoxPopuli As I understand it (from reading this) we cannot find a serializer for any object:
An ActiveModel::Serializer wraps a serializable resource and exposes an attributes method ...
So the method in question would have to at least have :serialized_hash
implemented, probably inherit ActiveModel::Model
or include ActiveModel::Serialization
.
Also: I've refined the test to show what I want to do with the class handed to the policy:
837df53
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.
right, but what is klass
right now? (this is a huge chance in improve the in-code docs).
- does it represent a serializable object? (PORO, AR, etc -- none of these need serialized_hash)
So, with your test, what happens if Comment
were defined as:
class Comment; 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.
Not sure what you mean - I've added a test to see if this doesn't mess up POROs - please take a look if this is what you're concerned about.
This is of course under the assumption that if an object is not serializable then it should not trigger the policy.
lib/active_model/serializer.rb
Outdated
elsif klass.superclass && ![BasicObject, Object].include?(klass.superclass) | ||
get_serializer_for(klass.superclass) | ||
elsif config.on_serializer_not_found.respond_to?(:call) && klass.respond_to?(:serialized_attributes) | ||
config.on_serializer_not_found.call(klass) |
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'm not sure how I feel about ending with situation 4 (as described in the method comments) as the result of an empty block.
Can you explain why you're checking for serialized_attributes in the elsif here, and what you would do if on_serializer_not_found is nil?
@@ -10,6 +10,7 @@ module Configuration | |||
config = base.config | |||
config.collection_serializer = ActiveModel::Serializer::CollectionSerializer | |||
config.serializer_lookup_enabled = true | |||
config.on_serializer_not_found = -> {} |
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.
what if this is nil?
should it start out as nil? does defaulting to an empty block feel weird?
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.
See comments above about the effects (TLDR: none). When I set the default here, I thought that maybe someone will be looking at the code here to consider the values for on_serializer_not_found
. I thought that an empty proc will communicate better that this was created to be (as opposed to nil
which could be anything).
ActiveModelSerializers.config.on_serializer_not_found = @previous_serializer_not_found_policy | ||
end | ||
|
||
def test_serializer_not_found_triggers_configured_policy |
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.
can you change these to the test 'serializer not found triggers configured policy'
test format.
Thanks!
lib/active_model/serializer.rb
Outdated
end | ||
end | ||
|
||
def self.excluded_base_class?(klass) |
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.
this method name doesn't make sense, it has a negative word, yet the condition is testing for the positive
lib/active_model/serializer.rb
Outdated
|
||
if serializer_class | ||
serializer_class | ||
elsif klass.superclass && !excluded_base_class?(klass.superclass) |
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.
see, where I'm getting caught up on this, is that I'm reading this as:
"if the objec i'm trying to serialize has a superclass, and that superclass is neither ActiveRecord::Base, BasicObject, nor Object, then get the serializer for that ....."
And here is the problem (in my looking things over too quickly -- you're getting the serializer for the superclass, not klass -- this makes sense now -- sorry about the confusion).
So, I guess for clarity, the condition should be extracted to a method -- maybe
`is_superclass_allowed_to_be_serializable?(klass)
but then, why don't we want those superclasses serializable? I know it's weird otherwise, but what if someone wanted to try to write a general serializer for Object
? would that be super weird? idk.
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.
The problem here is that I only need this additional if
condition to actually have a side-effect on the next if
, which is really why we're coming up against this kind of reasoning issues. I'll try to come up with a better solution.
lib/active_model/serializer.rb
Outdated
|
||
if serializer_class | ||
serializer_class | ||
elsif klass.superclass && find_serializer_class(klass.superclass) |
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.
this makes much more sense!
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.
also, I was jus about to suggest extracting the serializer_lookup to a method. :-)
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 - it was obvious after staring for 2 hours at it
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.
What happens if A < B < C
and there is a serializer for C
but not for B
?
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.
Sounds like we need to check ancestors?
elsif klass.superclass && find_serializer_class(klass.superclass) | ||
get_serializer_class(klass.superclass) | ||
elsif config.on_serializer_not_found.respond_to?(:call) && klass.respond_to?(:serialized_attributes) | ||
config.on_serializer_not_found.call(klass) | ||
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.
what happens if all the conditions are false? what do we do then?
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.
we return nil like we always did :)
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.
;-)
lib/active_model/serializer.rb
Outdated
end | ||
end | ||
|
||
def self.find_serializer_class(klass) | ||
serializer_lookup_chain_for(klass).map(&:safe_constantize).find { |x| x && x < ActiveModel::Serializer } |
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.
Can be lazified, i.e.:
serializer_lookup_chain_for(klass).lazy.map(&:safe_constantize).find { |x| x && x < ActiveModel::Serializer }
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.
Lazified.
b09d925
to
b3bf65a
Compare
@NullVoxPopuli @beauby I cleaned up the code and took care of the ancestor issues. Please take a look. |
@GregPK can you fix the rubocop issue real quick? |
lib/active_model/serializer.rb
Outdated
|
||
if serializer_class | ||
serializer_class | ||
elsif (ancestor_serializer_classes = ancestor_serializer_classes(klass)).present? |
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.
so this is change 1: s/klass.superclass/ancestor_serializer_classes(klass)
This is similar to code in either 0.8 or 0.9 IIRC
better Ruby to check for .any?
as you know you have a collection and you're checking if there's anything in it. present?
is one of those convinces Rails gives us that makes our code more less clear.
lib/active_model/serializer.rb
Outdated
serializer_class | ||
elsif (ancestor_serializer_classes = ancestor_serializer_classes(klass)).present? | ||
ancestor_serializer_classes.first | ||
elsif config.on_serializer_not_found.respond_to?(:call) && klass.respond_to?(:serialized_attributes) |
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.
and this is change 2: a new condition
Since we own this config, we shouldn't need to check if it responds to call. We can either make a default strategy or just check if the config is set. I think I like having a default strategy that does nothing an returns :no_serializer_found
What is serialized_attributes
that you are checking for?
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.
- The config setup you mentioned is probably the best way to go. However, if we setup a default strategy that returns a symbol, we will brake backwards compatibility for anyone relying on the historical behaviour of returning
nil
. - You're right about checking for callability. I think we can actually do
elsif config.on_serializer_not_found
- if it's an empty proc it will run, and when someone messes up the config value (ie setting it to:no_serializer_found
instead of-> { :no_serializer_found }
he will get an explicit error about what's wrong with the code. WDYT? - For
:serialized_attributes
, I'm checking whether this is actually something that represents a serializable AR resource. I'm aware that this excludes non AR-based objects. Maybe change this toklass.respond_to?(:serialized_attributes) || klass.respond_to?(:serialized_hash)
? This will handle both cases. I would much rather not couple this to AR viaserialized_attributes
but I could not find any other way of detecting is the object is both AR-based and serializable.
needs rebase |
Any updates on this? I'd love to see this get merged instead of having to add the monkey-patch (which is great btw.!) to our codebase. |
I suppose what I might like is a so, it would be something like: - return nil unless config.serializer_lookup_enabled
+ case config.serializer_lookup_enabled
+ when :strict_mode then fail StrictMode, "Won't lookup serializer for '#{klass}' In Strict mode serializers must be explicit"
+ when false then return nil
+ else nil # continue
+ end
serializers_cache.fetch_or_store(klass) do |
I thought about changing the approach to this problem, too. I tried by adding a last element to the lookup chain, that raises. The idea being: "if you reached this end of the lookup chain, there's no serializer for you". The only problem is that every element in the chain will be called for every lookup in one |
Truth is, relying on serializer inference incurs a massive perf drop, for a few saved keystrokes. |
What I have now running is a slightly modified version of the code in this pull request. Slightly modified in order to make it work with Rails 5. - elsif config.on_serializer_not_found.respond_to?(:call) && klass.respond_to?(:serialized_attributes)
+ elsif config.on_serializer_not_found.respond_to?(:call) && [ActiveRecord::Base, ApplicationRecord].include?(klass.superclass)
config.on_serializer_not_found.call(klass)
The change was necessary, because |
@beauby do we have benchmarks on this? |
b3bf65a
to
eba8e17
Compare
Purpose
Allow users to setup a policy to fail explicity when an
ActiveRecord::Base
inherited model is being used without an explicitSerializer
defined.Currently, when rendering a
ActiveRecord::Base
model it falls back on the default JSON serializer, exposing all attributes.Changes
ActiveModel::Serializer#get_serializer_for
Related GitHub issues