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

configurable serializer_lookup option #1757

Merged
merged 15 commits into from
Nov 16, 2016

Conversation

NullVoxPopuli
Copy link
Contributor

@NullVoxPopuli NullVoxPopuli commented May 30, 2016

Purpose

people need to be able to customize how the serializer look up is performed. Especially when it comes to API versioning, as there are quite a few ways to do serializer lookup and determining what way to map api version to serializers.

Changes

Adds a config option for adding additional serializer lookups.

Caveats

When configuring this, the dev can easily break everything.

Related GitHub issues

Additional helpful information

Things left to do:

  • Tests
    • Test for changing the Proc to have a different name matching scheme
    • Test to demonstrate taking the version from a request header
  • Benchmarks vs master
  • Documentation

lookups << '::' + current_namespace + '::' + resource_serializer
end

lookups
Copy link
Member

Choose a reason for hiding this comment

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

This should be in serialization_context, maybe?.

It's also pretty easy to solve with explicit code in the controller. then just pass in serializer: resource_serializer (or in index, each_serializer: resource_serializer) and you're good to go

    def resource_serializer
      instance_variable_get("@#{resource_name}_serializer") || self.resource_serializer = "#{self.class.parent}::#{resource_class.name}Serializer".constantize
    end

    def resource_serializer=(new_resource_serializer)
      instance_variable_set("@#{resource_name}_serializer", new_resource_serializer)
    end

    # The resource class based on the controller
    # @return [Class]
    def resource_class
      @resource_class ||= resource_name.classify.constantize
    end

    # The singular name for the resource class based on the controller
    # @return [String]
    def resource_name
      @resource_name ||= controller_name.singularize
    end

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is that what we want to recommend to people?

I think it's pretty straightforward to do this, but maybe it could be documented somewhere? So people who ask can work with something? Idk

The other thing I was thinking, is to Lee this option nil, so existing behavior remains the same, and provide the option?

But yeah, I'm on the fence on if ams should even provide the capability to configure the lookup.

Maybe, if rails pushes more API centric usage, it would wake more sense?

Copy link
Member

Choose a reason for hiding this comment

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

You guys could have a look at #1745. There is a discussion around the same idea (lookup based on controller). Maybe we can come up with a consensus on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh!, @bf4 that serializer lookup defined in the controller wouldn't keep relationships under the same namespace, would it? I think that would be harder to debug than the proc here

@NullVoxPopuli
Copy link
Contributor Author

needs rebase.

Is there a direction we want to go with this?
I think having serializer lookup being configurable would be a good way to help out people who have weird namespacing situations.

@NullVoxPopuli
Copy link
Contributor Author

@rails-api/ams ^

@NullVoxPopuli
Copy link
Contributor Author

rebased

@NullVoxPopuli
Copy link
Contributor Author

@bf4 @richmolj I'd like to revisit this, and get your thoughts on it.

What is present in this PR, is the change to support using a proc for serializer lookup and an example proc.
Obviously, the final version of this PR would have the proc be empty (if we wanted it that way).

I am in favor of the either of the following

  • Move serializer lookup entirely into the proc

or

  • have the proc be empty, keeping current / default lookup. The proc would only provide additional lookups, and the default would not be able to be changed (just prepended to the custom)

For both scenarios:

  • Provide tests that show procs and the common situations we get asked about with customizing the lookup chain. (API versioning, API fallback, etc)

@richmolj
Copy link
Contributor

richmolj commented Sep 9, 2016

@NullVoxPopuli after reading this comment of yours, I'm sold.

Move serializer lookup entirely into the proc

I'd favor a slight modification to this, moving the serializer lookup into a collection of procs. Something like:

ActiveModelSerializers.config.serializer_lookup_chain << proc_to_check_object_instance_method
ActiveModelSerializers.config.serializer_lookup_chain << proc_for_automatic_namespaced_lookup
ActiveModelSerializers.config.serializer_lookup_chain << proc_for_automatic_lookup

That way our internals match the externals (simpler), and users can easily re-order or add new ones that would take precedence. So basically middleware, where the first one to return a match wins.

# "::ResourceSerializer"]
config.serializer_lookup = proc { |controller_name|
lookups = []
current_resource = controller_name.gsub(/Controller\z/, '')
Copy link
Member

Choose a reason for hiding this comment

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

controller_name isn't correct. It's more lookup_namespace and then you want to lookup_namespace.remove(/Controller\z/) because you know someone might pass in a controller?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that's true. I'll switch it to lookup_namespace, and mention in a comment that it can and often is the controller name, cause of https://github.com/rails-api/active_model_serializers/pull/1757/files#diff-5624f9cf35be4dc254412d979fd293c4R63 klass is a controller (I guess, most of the time)

@NullVoxPopuli
Copy link
Contributor Author

NullVoxPopuli commented Sep 9, 2016

moving the serializer lookup into a collection of procs.

@richmolj: I think I could get on board with that -- but we need a place to store the procs that makes sense.

I looking at the existing code, I think it only makes sense to have the following as 'default':

  module AMS::SerializerLookup
     DEFAULT = -> (lookup_namespace) {
        chain = []

      resource_class_name = klass.name.demodulize
      resource_namespace = klass.name.deconstantize
      serializer_class_name = "#{resource_class_name}Serializer"

      # Example: 
      # BlogSerializer::AuthorSerializer
      chain.push("#{name}::#{serializer_class_name}") if self != ActiveModel::Serializer
      # Example: 
      # Api::AuthorSerializer
      chain.push("#{resource_namespace}::#{serializer_class_name}")

      chain
     }    
end

I don't think it makes sense to split this up, because both chain.push lines need serializer_class_name

then in serializer.rb:

    def self.serializer_lookup_chain_for(klass)
      ActiveModelSerializers.config.serializer_lookup_chain.map { |p| p.call(klass) }.flatten
    end

@NullVoxPopuli
Copy link
Contributor Author

I just realized, that these procs are going to need the serializer class name to be passed to them as well (esp for the case of defining nested serializers)

@richmolj
Copy link
Contributor

richmolj commented Sep 9, 2016

My thinking was something like

chain.push ->(klass) { klass.serializer if klass.respond_to?(:serializer) }
chain.push ->(klass) { "#{klass.name}Serializer".safe_constantize }
chain.push ->(klass) { "#{klass.name.demodulize}Serializer".safe_constantize }

Even better, pass an instance instead of the class and do the same.

But I'm splitting hairs here. As long as this is a customizable array of procs being passed an instance to be serialized, or the class of that instance, I'm 👍

@NullVoxPopuli
Copy link
Contributor Author

NullVoxPopuli commented Sep 9, 2016

I think it the interface would have to be:

String, Class
-> (lookup_namespace, serializer_class) { ... }

Because, serializer_lookup_chain_for only gets passed the controller class (not the instance). I also thinking passing the instance would invite too much coupling.

But, with the above change to the proc API, the procs could be:

# Downside to splitting this up so much, is that there is some duplicate code
# Like creating the serializer name string from since it's going to be the suffix
# of everything
# This could be a third parameter so it's not calculated all the time.
# however, this proc would then have 3 parameters, and I don't know how I feel about that.
module AMS::SerializerLookup
   DEFAULT = [
      IN_PARENT_SERIALIZER
      BY_NAMESPACE
   ] 

  IN_PARENT_SERIALIZER = -> (lookup_namespace, serializer_class) {
    # logic for getting the parent::child serializer name
  }

  BY_NAMESPACE = -> (lookup_namespace, serializer_class) {
    # logic for getting a serializer of the same namespace as lookup_namespace
  }
end
# serializer.rb
    def self.serializer_lookup_chain_for(klass)
      ActiveModelSerializers.config.serializer_lookup_chain.map { |p| 
        p.call(klass, self) 
      }.flatten.compact
    end

Config Somewhere:

# ( already default (in configuration.rb), so this would be redundant )
AMS.config.serializer_lookup_chain = AMS::SerializerLookup::DEFAULT
# or 
AMS.config.serializer_lookup_chain << whatever_proc
# or
AMS.config.serializer_lookup_chain = []
AMS.config.serializer_lookup_chain << some_proc

@richmolj
Copy link
Contributor

richmolj commented Sep 9, 2016

Because, serializer_lookup_chain_for only gets passed the controller class (not the instance).

Right, but I think we could change this line pretty easily. However, you note the performance impact of string instantiation which I agree with. It looks like the result is cached so it's not a performance issue if we pass the class, but it would be if we passed the instance.

So I can get behind the idea of passing the class, instead of the instance, for that reason. My only thought with the instance was you could pick the serializer at runtime based on a property or something.

I'm not totally sure why lookup_namespace is needed in addition to the class though.

@richmolj
Copy link
Contributor

richmolj commented Sep 9, 2016

Oh hold on I think I may have found the source of my confusion. The goal is to based the lookup on the controller namespace, not the object's namespace. That makes sense now. My preference would be to pass the controller instance itself, instead of just its namespace, but either way.

@NullVoxPopuli
Copy link
Contributor Author

I'll get some tests written for this, this weekend to demonstrate the different scenarios I can think of where this could be useful.

@richmolj
Copy link
Contributor

richmolj commented Sep 9, 2016

After thinking about it a little bit more, I think you were right. lookup_namespace and serializer_klass should be the arguments passed to the procs. Then use the caching in serializer_lookup_chain_for to avoid a performance penalty.

This is a great idea. I don't have this use case yet, but I expect to eventually.


lookups = ActiveModelSerializers.config.serializer_lookup_chain
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@richmolj this looks nice - It's already chached, see line 86

@NullVoxPopuli
Copy link
Contributor Author

At the very least all this is finally getting documented

@NullVoxPopuli
Copy link
Contributor Author

@remear do you have opinions on this?

describe 'configuration' do
describe 'serializer lookup' do
describe 'using SerializableResource' do
it 'uses the child serializer' do
Copy link
Contributor Author

Choose a reason for hiding this comment

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

more tests need to exist in this file such that it can be the example of possible lookup chain scenarios.

Including:

  • Versioned APIs (both via URL and request header)
  • Versioned APIs with fallback to earlier version
  • Mimicing the controller's namespace.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this'll be todo, as we'd just be testing different ways to configure -- which is open ended

@groyoh
Copy link
Member

groyoh commented Nov 15, 2016

@NullVoxPopuli have a look at https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model/serializer/collection_serializer.rb#L74. We do not pass the namespace options when initializing the serializers meaning that if we have:

module Api
  module V1
    class PrimaryResourceSerializer < ActiveModel::Serializer
      attributes :title, :body
      has_many :has_many_relationships
    end
    class HasManyRelationshipSerializer < ActiveModel::Serializer
      attribute :body
    end
  end
end

The has_many lookup chain will be will be

[
  "AmsBench::Api::V1::PrimaryResourceSerializer::HasManyRelationshipSerializer",   
  "::HasManyRelationshipSerializer",
  "HasManyRelationshipSerializer"
]

instead of

[
  "AmsBench::Api::V1::PrimaryResourceSerializer::HasManyRelationshipSerializer",   
  "AmsBench::Api::V1::HasManyRelationshipSerializer",
  "HasManyRelationshipSerializer"
]

Pretty sure that's a bug but don't have time right now to look further into it. Will check again later. Also not sure if it belongs to this PR either or a different bug PR.

@NullVoxPopuli
Copy link
Contributor Author

Oh, collections. Kk. Good find. I can make a PR to fix soon

On Tue, Nov 15, 2016, 5:05 AM Yohan Robert [email protected] wrote:

@NullVoxPopuli https://github.com/NullVoxPopuli have a look at
https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model/serializer/collection_serializer.rb#L74.
We do not pass the namespace options when initializing the serializers
meaning that if we have:

module Api
module V1
class PrimaryResourceSerializer < ActiveModel::Serializer
attributes :title, :body
has_many :has_many_relationships
end
class HasManyRelationshipSerializer < ActiveModel::Serializer
attribute :body
end
endend

The has_many lookup chain will be will be

[
"AmsBench::Api::V1::PrimaryResourceSerializer::HasManyRelationshipSerializer",
"::HasManyRelationshipSerializer",
"HasManyRelationshipSerializer"
]

instead of

[
"AmsBench::Api::V1::PrimaryResourceSerializer::HasManyRelationshipSerializer",
"AmsBench::Api::V1::HasManyRelationshipSerializer",
"HasManyRelationshipSerializer"
]

Pretty sure that's a bug but don't have time right now to look further
into it. Will check again later. Also not sure if it belongs to this PR
either or a different bug PR.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#1757 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAMJaoUs76jVIsSY03KwRgD0x2a_Yh33ks5q-YPrgaJpZM4Ipy62
.

@groyoh
Copy link
Member

groyoh commented Nov 15, 2016

Once #1973 is merged, I'll add my benchmark changes/results. Otherwise the benchmark won't be accurate.

@NullVoxPopuli
Copy link
Contributor Author

cool. I'm just waiting for / watching travis

@NullVoxPopuli
Copy link
Contributor Author

@groyoh #1973 is merged

@groyoh
Copy link
Member

groyoh commented Nov 15, 2016

This is the final results:

{
  "label": "Configurable Lookup Chain",
  "version": "0.10.2",
  "rails_version": "4.2.7.1",
  "iterations_per_second": 2104.3974604253326,
  "iterations_per_second_standard_deviation": 2.280937936044573,
  "total_allocated_objects_per_iteration": 593
}
{
  "label": "Old Lookup Chain (v0.10)",
  "version": "0.10.2",
  "rails_version": "4.2.7.1",
  "iterations_per_second": 2097.130687654142,
  "iterations_per_second_standard_deviation": 6.055893452308528,
  "total_allocated_objects_per_iteration": 593
}

@groyoh
Copy link
Member

groyoh commented Nov 15, 2016

I just figured out there is something strange with the benchmark. I need to test it further.

@NullVoxPopuli
Copy link
Contributor Author

bro. rubocop

@NullVoxPopuli
Copy link
Contributor Author

I wish I could 👍 a commit

@NullVoxPopuli
Copy link
Contributor Author

wait, so the old lookup chain is slower than the new configurable one? with the same objects? wat

@NullVoxPopuli
Copy link
Contributor Author

anywho, that benchmark looks good to me 👍

#
# If CustomNamespace::ResourceSerializer exists, it will be used
# for serialization
config.serializer_lookup_chain = ActiveModelSerializers::LookupChain::DEFAULT.dup
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the .dup?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

DEFAULT is a frozen array, and we want mutatability?

Copy link
Contributor

Choose a reason for hiding this comment

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

DEFAULT should be frozen, but are there real use cases for injecting stuff in the lookup chain rather than replacing it altogether?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not entirely sure on that one.

Do we get a performance boost if serializer_lookup_chain is frozen? I wouldn't think it matters, since config is a singletonish thing

@groyoh
Copy link
Member

groyoh commented Nov 15, 2016

@NullVoxPopuli turns out the whole serializer lookup is cached (https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model/serializer.rb#L97) so we cannot see any difference once it's cached...in the end the benchmark I added does not actually says much.

@NullVoxPopuli
Copy link
Contributor Author

Excellent

On Tue, Nov 15, 2016, 6:37 PM Yohan Robert [email protected] wrote:

@NullVoxPopuli https://github.com/NullVoxPopuli turns out the whole
serializer lookup is cached (
https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model/serializer.rb#L97)
so we cannot see any difference once it's cached...in the end the benchmark
I added does not actually says much.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#1757 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAMJagqJWxSoaBVFjlLZCTQ50AELgZo5ks5q-kIbgaJpZM4Ipy62
.

@NullVoxPopuli
Copy link
Contributor Author

any objections to merging?

@groyoh
Copy link
Member

groyoh commented Nov 16, 2016

Yup, sorry gotta push a last commit. Will do it in 5min (and I will make sure I run rubocop 😁 ).

@groyoh
Copy link
Member

groyoh commented Nov 16, 2016

So the end result is:

  • if I have this code
configurable.call
old.call
{
  "label": "Configurable Lookup Chain",
  "version": "0.10.2",
  "rails_version": "4.2.7.1",
  "iterations_per_second": 2097.0843979297792,
  "iterations_per_second_standard_deviation": 2.6226889129638966,
  "total_allocated_objects_per_iteration": 593
}
{
  "label": "Old Lookup Chain (v0.10)",
  "version": "0.10.2",
  "rails_version": "4.2.7.1",
  "iterations_per_second": 2140.142899144878,
  "iterations_per_second_standard_deviation": 3.831519850041984,
  "total_allocated_objects_per_iteration": 593
}
  • if I have this code
old.call
configurable.call
{
  "label": "Old Lookup Chain (v0.10)",
  "version": "0.10.2",
  "rails_version": "4.2.7.1",
  "iterations_per_second": 2103.7895845497637,
  "iterations_per_second_standard_deviation": 2.8044629763972666,
  "total_allocated_objects_per_iteration": 593
}
{
  "label": "Configurable Lookup Chain",
  "version": "0.10.2",
  "rails_version": "4.2.7.1",
  "iterations_per_second": 2177.0852661503927,
  "iterations_per_second_standard_deviation": 1.3779892072416242,
  "total_allocated_objects_per_iteration": 593
}

So it looks that the one running last is running faster (?). But looking at both result I'd say the difference is not significant.

@NullVoxPopuli
Copy link
Contributor Author

cool. Running the old one actually would invalidate the results of the configurable lookup chain, because of the in-bench monkey patch.

But excellent! once travis passes, lets merge.

@groyoh
Copy link
Member

groyoh commented Nov 16, 2016

So I'll merge once Travis give the go and no one objects!

@groyoh
Copy link
Member

groyoh commented Nov 16, 2016

@NullVoxPopuli in my last commit I make sure to clear the cache before each benchmark. That way we ensure that the lookup chain method is actually called and that both benchmark are similar.

@groyoh groyoh merged commit d31d741 into rails-api:master Nov 16, 2016
@NullVoxPopuli NullVoxPopuli deleted the serializer-lookup branch January 31, 2017 13:18
GregPK pushed a commit to GregPK/active_model_serializers that referenced this pull request Apr 25, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants