mirror of
https://github.com/ditkrg/active_model_serializers.git
synced 2026-01-22 22:06:50 +00:00
Make serializer lookup configurable (#1757)
This commit is contained in:
parent
d0de53cbb2
commit
d31d741f43
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -60,17 +60,10 @@ module ActiveModel
|
||||
|
||||
# @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
|
||||
|
||||
@ -32,6 +32,26 @@ module ActiveModel
|
||||
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
|
||||
|
||||
@ -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))
|
||||
|
||||
80
lib/active_model_serializers/lookup_chain.rb
Normal file
80
lib/active_model_serializers/lookup_chain.rb
Normal file
@ -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
test/action_controller/lookup_proc_test.rb
Normal file
49
test/action_controller/lookup_proc_test.rb
Normal file
@ -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
|
||||
@ -15,6 +15,16 @@ module ActionController
|
||||
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
|
||||
@ -92,6 +102,14 @@ module ActionController
|
||||
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
|
||||
@ -102,6 +120,13 @@ module ActionController
|
||||
@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
|
||||
|
||||
|
||||
83
test/benchmark/bm_lookup_chain.rb
Normal file
83
test/benchmark/bm_lookup_chain.rb
Normal file
@ -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
|
||||
@ -17,6 +17,14 @@ module SerializationTesting
|
||||
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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user