Skip to content

Commit

Permalink
Make serializer lookup configurable (#1757)
Browse files Browse the repository at this point in the history
  • Loading branch information
NullVoxPopuli authored and Yohan Robert committed Nov 16, 2016
1 parent d0de53c commit d31d741
Show file tree
Hide file tree
Showing 10 changed files with 321 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Fixes:

Features:

- [#1757](https://github.com/rails-api/active_model_serializers/pull/1757) Make serializer lookup chain configurable. (@NullVoxPopuli)
- [#1968](https://github.com/rails-api/active_model_serializers/pull/1968) (@NullVoxPopuli)
- Add controller namespace to default controller lookup
- Provide a `namespace` render option
Expand Down
50 changes: 50 additions & 0 deletions docs/general/configuration_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,56 @@ application, setting `config.key_transform` to `:unaltered` will provide a perfo
What relationships to serialize by default. Default: `'*'`, which includes one level of related
objects. See [includes](adapters.md#included) for more info.


##### serializer_lookup_chain

Configures how serializers are searched for. By default, the lookup chain is

```ruby
ActiveModelSerializers::LookupChain::DEFAULT
```

which is shorthand for

```ruby
[
ActiveModelSerializers::LookupChain::BY_PARENT_SERIALIZER,
ActiveModelSerializers::LookupChain::BY_NAMESPACE,
ActiveModelSerializers::LookupChain::BY_RESOURCE_NAMESPACE,
ActiveModelSerializers::LookupChain::BY_RESOURCE
]
```

Each of the array entries represent a proc. A serializer lookup proc will be yielded 3 arguments. `resource_class`, `serializer_class`, and `namespace`.

Note that:
- `resource_class` is the class of the resource being rendered
- by default `serializer_class` is `ActiveModel::Serializer`
- for association lookup it's the "parent" serializer
- `namespace` correspond to either the controller namespace or the [optionally] specified [namespace render option](./rendering.md#namespace)

An example config could be:

```ruby
ActiveModelSerializers.config.serializer_lookup_chain = [
lambda do |resource_class, serializer_class, namespace|
"API::#{namespace}::#{resource_class}"
end
]
```

If you simply want to add to the existing lookup_chain. Use `unshift`.

```ruby
ActiveModelSerializers.config.serializer_lookup_chain.unshift(
lambda do |resource_class, serializer_class, namespace|
# ...
end
)
```

See [lookup_chain.rb](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model_serializers/lookup_chain.rb) for further explanations and examples.

## JSON API

##### jsonapi_resource_type
Expand Down
15 changes: 4 additions & 11 deletions lib/active_model/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,10 @@ class << self

# @api private
def self.serializer_lookup_chain_for(klass, namespace = nil)
chain = []

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

chain.push("#{namespace}::#{serializer_class_name}") if namespace
chain.push("#{name}::#{serializer_class_name}") if self != ActiveModel::Serializer
chain.push("#{resource_namespace}::#{serializer_class_name}")

chain
lookups = ActiveModelSerializers.config.serializer_lookup_chain
Array[*lookups].flat_map do |lookup|
lookup.call(klass, self, namespace)
end.compact
end

# Used to cache serializer name => serializer class
Expand Down
20 changes: 20 additions & 0 deletions lib/active_model/serializer/concerns/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@ def config.array_serializer
config.jsonapi_include_toplevel_object = false
config.include_data_default = true

# For configuring how serializers are found.
# This should be an array of procs.
#
# The priority of the output is that the first item
# in the evaluated result array will take precedence
# over other possible serializer paths.
#
# i.e.: First match wins.
#
# @example output
# => [
# "CustomNamespace::ResourceSerializer",
# "ParentSerializer::ResourceSerializer",
# "ResourceNamespace::ResourceSerializer" ,
# "ResourceSerializer"]
#
# If CustomNamespace::ResourceSerializer exists, it will be used
# for serialization
config.serializer_lookup_chain = ActiveModelSerializers::LookupChain::DEFAULT.dup

config.schema_path = 'test/support/schemas'
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/active_model_serializers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module ActiveModelSerializers
autoload :Adapter
autoload :JsonPointer
autoload :Deprecate
autoload :LookupChain

class << self; attr_accessor :logger; end
self.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
Expand Down
80 changes: 80 additions & 0 deletions lib/active_model_serializers/lookup_chain.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
module ActiveModelSerializers
module LookupChain
# Standard appending of Serializer to the resource name.
#
# Example:
# Author => AuthorSerializer
BY_RESOURCE = lambda do |resource_class, _serializer_class, _namespace|
serializer_from(resource_class)
end

# Uses the namespace of the resource to find the serializer
#
# Example:
# British::Author => British::AuthorSerializer
BY_RESOURCE_NAMESPACE = lambda do |resource_class, _serializer_class, _namespace|
resource_namespace = namespace_for(resource_class)
serializer_name = serializer_from(resource_class)

"#{resource_namespace}::#{serializer_name}"
end

# Uses the controller namespace of the resource to find the serializer
#
# Example:
# Api::V3::AuthorsController => Api::V3::AuthorSerializer
BY_NAMESPACE = lambda do |resource_class, _serializer_class, namespace|
resource_name = resource_class_name(resource_class)
namespace ? "#{namespace}::#{resource_name}Serializer" : nil
end

# Allows for serializers to be defined in parent serializers
# - useful if a relationship only needs a different set of attributes
# than if it were rendered independently.
#
# Example:
# class BlogSerializer < ActiveModel::Serializer
# class AuthorSerialier < ActiveModel::Serializer
# ...
# end
#
# belongs_to :author
# ...
# end
#
# The belongs_to relationship would be rendered with
# BlogSerializer::AuthorSerialier
BY_PARENT_SERIALIZER = lambda do |resource_class, serializer_class, _namespace|
return if serializer_class == ActiveModel::Serializer

serializer_name = serializer_from(resource_class)
"#{serializer_class}::#{serializer_name}"
end

DEFAULT = [
BY_PARENT_SERIALIZER,
BY_NAMESPACE,
BY_RESOURCE_NAMESPACE,
BY_RESOURCE
].freeze

module_function

def namespace_for(klass)
klass.name.deconstantize
end

def resource_class_name(klass)
klass.name.demodulize
end

def serializer_from_resource_name(name)
"#{name}Serializer"
end

def serializer_from(klass)
name = resource_class_name(klass)
serializer_from_resource_name(name)
end
end
end
49 changes: 49 additions & 0 deletions test/action_controller/lookup_proc_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require 'test_helper'

module ActionController
module Serialization
class LookupProcTest < ActionController::TestCase
module Api
module V3
class PostCustomSerializer < ActiveModel::Serializer
attributes :title, :body

belongs_to :author
end

class AuthorCustomSerializer < ActiveModel::Serializer
attributes :name
end

class LookupProcTestController < ActionController::Base
def implicit_namespaced_serializer
author = Author.new(name: 'Bob')
post = Post.new(title: 'New Post', body: 'Body', author: author)

render json: post
end
end
end
end

tests Api::V3::LookupProcTestController

test 'implicitly uses namespaced serializer' do
controller_namespace = lambda do |resource_class, _parent_serializer_class, namespace|
"#{namespace}::#{resource_class}CustomSerializer" if namespace
end

with_prepended_lookup(controller_namespace) do
get :implicit_namespaced_serializer

assert_serializer Api::V3::PostCustomSerializer

expected = { 'title' => 'New Post', 'body' => 'Body', 'author' => { 'name' => 'Bob' } }
actual = JSON.parse(@response.body)

assert_equal expected, actual
end
end
end
end
end
25 changes: 25 additions & 0 deletions test/action_controller/namespace_lookup_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ class BookSerializer < ActiveModel::Serializer
end
end

module VHeader
class BookSerializer < ActiveModel::Serializer
attributes :title, :body

def body
'header'
end
end
end

module V3
class BookSerializer < ActiveModel::Serializer
attributes :title, :body
Expand Down Expand Up @@ -92,6 +102,14 @@ def namespace_set_in_before_filter
book = Book.new(title: 'New Post', body: 'Body')
render json: book
end

def namespace_set_by_request_headers
book = Book.new(title: 'New Post', body: 'Body')
version_from_header = request.headers['X-API_VERSION']
namespace = "ActionController::Serialization::NamespaceLookupTest::#{version_from_header}"

render json: book, namespace: namespace
end
end
end
end
Expand All @@ -102,6 +120,13 @@ def namespace_set_in_before_filter
@test_namespace = self.class.parent
end

test 'uses request headers to determine the namespace' do
request.env['X-API_VERSION'] = 'Api::VHeader'
get :namespace_set_by_request_headers

assert_serializer Api::VHeader::BookSerializer
end

test 'implicitly uses namespaced serializer' do
get :implicit_namespaced_serializer

Expand Down
83 changes: 83 additions & 0 deletions test/benchmark/bm_lookup_chain.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
require_relative './benchmarking_support'
require_relative './app'

time = 10
disable_gc = true
ActiveModelSerializers.config.key_transform = :unaltered

module AmsBench
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
class PrimaryResourceSerializer < ActiveModel::Serializer
attributes :title, :body

has_many :has_many_relationships

class HasManyRelationshipSerializer < ActiveModel::Serializer
attribute :body
end
end
end

resource = PrimaryResource.new(
id: 1,
title: 'title',
body: 'body',
has_many_relationships: [
HasManyRelationship.new(id: 1, body: 'body1'),
HasManyRelationship.new(id: 2, body: 'body1')
]
)

serialization = lambda do
ActiveModelSerializers::SerializableResource.new(resource, serializer: AmsBench::PrimaryResourceSerializer).as_json
ActiveModelSerializers::SerializableResource.new(resource, namespace: AmsBench::Api::V1).as_json
ActiveModelSerializers::SerializableResource.new(resource).as_json
end

def clear_cache
AmsBench::PrimaryResourceSerializer.serializers_cache.clear
AmsBench::Api::V1::PrimaryResourceSerializer.serializers_cache.clear
ActiveModel::Serializer.serializers_cache.clear
end

configurable = lambda do
clear_cache
Benchmark.ams('Configurable Lookup Chain', time: time, disable_gc: disable_gc, &serialization)
end

old = lambda do
clear_cache
module ActiveModel
class Serializer
def self.serializer_lookup_chain_for(klass, namespace = nil)
chain = []

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

chain.push("#{namespace}::#{serializer_class_name}") if namespace
chain.push("#{name}::#{serializer_class_name}") if self != ActiveModel::Serializer
chain.push("#{resource_namespace}::#{serializer_class_name}")
chain
end
end
end

Benchmark.ams('Old Lookup Chain (v0.10)', time: time, disable_gc: disable_gc, &serialization)
end

configurable.call
old.call
8 changes: 8 additions & 0 deletions test/support/serialization_testing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ def with_namespace_separator(separator)
ActiveModelSerializers.config.jsonapi_namespace_separator = original_separator
end

def with_prepended_lookup(lookup_proc)
original_lookup = ActiveModelSerializers.config.serializer_lookup_cahin
ActiveModelSerializers.config.serializer_lookup_chain.unshift lookup_proc
yield
ensure
ActiveModelSerializers.config.serializer_lookup_cahin = original_lookup
end

# Aliased as :with_configured_adapter to clarify that
# this method tests the configured adapter.
# When not testing configuration, it may be preferable
Expand Down

0 comments on commit d31d741

Please sign in to comment.