mirror of
https://github.com/ditkrg/active_model_serializers.git
synced 2026-01-22 22:06:50 +00:00
This adds namespace lookup to serializer_for (#1968)
* This adds namespace lookup to serializer_for * address rubocop issue * address @bf4's feedback * add docs * update docs, add more tests * apparently rails master doesn't have before filter * try to address serializer cache issue between tests * update cache for serializer lookup to include namespace in the key, and fix the tests for explicit namespace * update docs, and use better cache key creation method * update docs [ci skip] * update docs [ci skip] * add to changelog [ci skip]
This commit is contained in:
parent
b709cd41e6
commit
b29395b0ac
@ -20,7 +20,8 @@ cache:
|
||||
|
||||
script:
|
||||
- bundle exec rake ci
|
||||
|
||||
after_success:
|
||||
- codeclimate-test-reporter
|
||||
env:
|
||||
global:
|
||||
- "JRUBY_OPTS='--dev -J-Xmx1024M --debug'"
|
||||
|
||||
@ -13,6 +13,10 @@ Fixes:
|
||||
|
||||
Features:
|
||||
|
||||
- [#1968](https://github.com/rails-api/active_model_serializers/pull/1968) (@NullVoxPopuli)
|
||||
- Add controller namespace to default controller lookup
|
||||
- Provide a `namespace` render option
|
||||
- document how set the namespace in the controller for implicit lookup.
|
||||
- [#1791](https://github.com/rails-api/active_model_serializers/pull/1791) (@bf4, @youroff, @NullVoxPopuli)
|
||||
- Added `jsonapi_namespace_separator` config option.
|
||||
- [#1889](https://github.com/rails-api/active_model_serializers/pull/1889) Support key transformation for Attributes adapter (@iancanderson, @danbee)
|
||||
|
||||
@ -243,6 +243,36 @@ This will be rendered as:
|
||||
```
|
||||
Note: the `Attributes` adapter (default) does not include a resource root. You also will not be able to create a single top-level root if you are using the :json_api adapter.
|
||||
|
||||
#### namespace
|
||||
|
||||
The namespace for serializer lookup is based on the controller.
|
||||
|
||||
To configure the implicit namespace, in your controller, create a before filter
|
||||
|
||||
```ruby
|
||||
before_action do
|
||||
self.namespace_for_serializer = Api::V2
|
||||
end
|
||||
```
|
||||
|
||||
`namespace` can also be passed in as a render option:
|
||||
|
||||
|
||||
```ruby
|
||||
@post = Post.first
|
||||
render json: @post, namespace: Api::V2
|
||||
```
|
||||
|
||||
This tells the serializer lookup to check for the existence of `Api::V2::PostSerializer`, and if any relations are rendered with `@post`, they will also utilize the `Api::V2` namespace.
|
||||
|
||||
The `namespace` can be any object whose namespace can be represented by string interpolation (i.e. by calling to_s)
|
||||
- Module `Api::V2`
|
||||
- String `'Api::V2'`
|
||||
- Symbol `:'Api::V2'`
|
||||
|
||||
Note that by using a string and symbol, Ruby will assume the namespace is defined at the top level.
|
||||
|
||||
|
||||
#### serializer
|
||||
|
||||
PR please :)
|
||||
|
||||
@ -16,6 +16,12 @@ module ActionController
|
||||
included do
|
||||
class_attribute :_serialization_scope
|
||||
self._serialization_scope = :current_user
|
||||
|
||||
attr_writer :namespace_for_serializer
|
||||
end
|
||||
|
||||
def namespace_for_serializer
|
||||
@namespace_for_serializer ||= self.class.parent unless self.class.parent == Object
|
||||
end
|
||||
|
||||
def serialization_scope
|
||||
@ -30,6 +36,9 @@ module ActionController
|
||||
"Please pass 'adapter: false' or see ActiveSupport::SerializableResource.new"
|
||||
options[:adapter] = false
|
||||
end
|
||||
|
||||
options.fetch(:namespace) { options[:namespace] = namespace_for_serializer }
|
||||
|
||||
serializable_resource = ActiveModelSerializers::SerializableResource.new(resource, options)
|
||||
serializable_resource.serialization_scope ||= options.fetch(:scope) { serialization_scope }
|
||||
serializable_resource.serialization_scope_name = options.fetch(:scope_name) { _serialization_scope }
|
||||
|
||||
@ -44,7 +44,7 @@ module ActiveModel
|
||||
elsif resource.respond_to?(:to_ary)
|
||||
config.collection_serializer
|
||||
else
|
||||
options.fetch(:serializer) { get_serializer_for(resource.class) }
|
||||
options.fetch(:serializer) { get_serializer_for(resource.class, options[:namespace]) }
|
||||
end
|
||||
end
|
||||
|
||||
@ -59,13 +59,14 @@ module ActiveModel
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.serializer_lookup_chain_for(klass)
|
||||
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}")
|
||||
|
||||
@ -84,11 +85,14 @@ module ActiveModel
|
||||
# 1. class name appended with "Serializer"
|
||||
# 2. try again with superclass, if present
|
||||
# 3. nil
|
||||
def self.get_serializer_for(klass)
|
||||
def self.get_serializer_for(klass, namespace = nil)
|
||||
return nil unless config.serializer_lookup_enabled
|
||||
serializers_cache.fetch_or_store(klass) do
|
||||
|
||||
cache_key = ActiveSupport::Cache.expand_cache_key(klass, namespace)
|
||||
serializers_cache.fetch_or_store(cache_key) do
|
||||
# NOTE(beauby): When we drop 1.9.3 support we can lazify the map for perfs.
|
||||
serializer_class = serializer_lookup_chain_for(klass).map(&:safe_constantize).find { |x| x && x < ActiveModel::Serializer }
|
||||
lookup_chain = serializer_lookup_chain_for(klass, namespace)
|
||||
serializer_class = lookup_chain.map(&:safe_constantize).find { |x| x && x < ActiveModel::Serializer }
|
||||
|
||||
if serializer_class
|
||||
serializer_class
|
||||
|
||||
@ -106,6 +106,10 @@ module ActiveModel
|
||||
#
|
||||
def build_association(parent_serializer, parent_serializer_options, include_slice = {})
|
||||
reflection_options = options.dup
|
||||
|
||||
# Pass the parent's namespace onto the child serializer
|
||||
reflection_options[:namespace] ||= parent_serializer_options[:namespace]
|
||||
|
||||
association_value = value(parent_serializer, include_slice)
|
||||
serializer_class = parent_serializer.class.serializer_for(association_value, reflection_options)
|
||||
reflection_options[:include_data] = include_data?(include_slice)
|
||||
|
||||
@ -55,7 +55,7 @@ module ActiveModelSerializers
|
||||
@serializer ||=
|
||||
begin
|
||||
@serializer = serializer_opts.delete(:serializer)
|
||||
@serializer ||= ActiveModel::Serializer.serializer_for(resource)
|
||||
@serializer ||= ActiveModel::Serializer.serializer_for(resource, serializer_opts)
|
||||
|
||||
if serializer_opts.key?(:each_serializer)
|
||||
serializer_opts[:serializer] = serializer_opts.delete(:each_serializer)
|
||||
|
||||
149
test/action_controller/namespace_lookup_test.rb
Normal file
149
test/action_controller/namespace_lookup_test.rb
Normal file
@ -0,0 +1,149 @@
|
||||
require 'test_helper'
|
||||
|
||||
module ActionController
|
||||
module Serialization
|
||||
class NamespaceLookupTest < ActionController::TestCase
|
||||
class Book < ::Model; end
|
||||
class Page < ::Model; end
|
||||
class Writer < ::Model; end
|
||||
|
||||
module Api
|
||||
module V2
|
||||
class BookSerializer < ActiveModel::Serializer
|
||||
attributes :title
|
||||
end
|
||||
end
|
||||
|
||||
module V3
|
||||
class BookSerializer < ActiveModel::Serializer
|
||||
attributes :title, :body
|
||||
|
||||
belongs_to :writer
|
||||
end
|
||||
|
||||
class WriterSerializer < ActiveModel::Serializer
|
||||
attributes :name
|
||||
end
|
||||
|
||||
class LookupTestController < ActionController::Base
|
||||
before_action only: [:namespace_set_in_before_filter] do
|
||||
self.namespace_for_serializer = Api::V2
|
||||
end
|
||||
|
||||
def implicit_namespaced_serializer
|
||||
writer = Writer.new(name: 'Bob')
|
||||
book = Book.new(title: 'New Post', body: 'Body', writer: writer)
|
||||
|
||||
render json: book
|
||||
end
|
||||
|
||||
def explicit_namespace_as_module
|
||||
book = Book.new(title: 'New Post', body: 'Body')
|
||||
|
||||
render json: book, namespace: Api::V2
|
||||
end
|
||||
|
||||
def explicit_namespace_as_string
|
||||
book = Book.new(title: 'New Post', body: 'Body')
|
||||
|
||||
# because this is a string, ruby can't auto-lookup the constant, so otherwise
|
||||
# the looku things we mean ::Api::V2
|
||||
render json: book, namespace: 'ActionController::Serialization::NamespaceLookupTest::Api::V2'
|
||||
end
|
||||
|
||||
def explicit_namespace_as_symbol
|
||||
book = Book.new(title: 'New Post', body: 'Body')
|
||||
|
||||
# because this is a string, ruby can't auto-lookup the constant, so otherwise
|
||||
# the looku things we mean ::Api::V2
|
||||
render json: book, namespace: :'ActionController::Serialization::NamespaceLookupTest::Api::V2'
|
||||
end
|
||||
|
||||
def invalid_namespace
|
||||
book = Book.new(title: 'New Post', body: 'Body')
|
||||
|
||||
render json: book, namespace: :api_v2
|
||||
end
|
||||
|
||||
def namespace_set_in_before_filter
|
||||
book = Book.new(title: 'New Post', body: 'Body')
|
||||
render json: book
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
tests Api::V3::LookupTestController
|
||||
|
||||
setup do
|
||||
@test_namespace = self.class.parent
|
||||
end
|
||||
|
||||
test 'implicitly uses namespaced serializer' do
|
||||
get :implicit_namespaced_serializer
|
||||
|
||||
assert_serializer Api::V3::BookSerializer
|
||||
|
||||
expected = { 'title' => 'New Post', 'body' => 'Body', 'writer' => { 'name' => 'Bob' } }
|
||||
actual = JSON.parse(@response.body)
|
||||
|
||||
assert_equal expected, actual
|
||||
end
|
||||
|
||||
test 'explicit namespace as module' do
|
||||
get :explicit_namespace_as_module
|
||||
|
||||
assert_serializer Api::V2::BookSerializer
|
||||
|
||||
expected = { 'title' => 'New Post' }
|
||||
actual = JSON.parse(@response.body)
|
||||
|
||||
assert_equal expected, actual
|
||||
end
|
||||
|
||||
test 'explicit namespace as string' do
|
||||
get :explicit_namespace_as_string
|
||||
|
||||
assert_serializer Api::V2::BookSerializer
|
||||
|
||||
expected = { 'title' => 'New Post' }
|
||||
actual = JSON.parse(@response.body)
|
||||
|
||||
assert_equal expected, actual
|
||||
end
|
||||
|
||||
test 'explicit namespace as symbol' do
|
||||
get :explicit_namespace_as_symbol
|
||||
|
||||
assert_serializer Api::V2::BookSerializer
|
||||
|
||||
expected = { 'title' => 'New Post' }
|
||||
actual = JSON.parse(@response.body)
|
||||
|
||||
assert_equal expected, actual
|
||||
end
|
||||
|
||||
test 'invalid namespace' do
|
||||
get :invalid_namespace
|
||||
|
||||
assert_serializer ActiveModel::Serializer::Null
|
||||
|
||||
expected = { 'title' => 'New Post', 'body' => 'Body' }
|
||||
actual = JSON.parse(@response.body)
|
||||
|
||||
assert_equal expected, actual
|
||||
end
|
||||
|
||||
test 'namespace set in before filter' do
|
||||
get :namespace_set_in_before_filter
|
||||
|
||||
assert_serializer Api::V2::BookSerializer
|
||||
|
||||
expected = { 'title' => 'New Post' }
|
||||
actual = JSON.parse(@response.body)
|
||||
|
||||
assert_equal expected, actual
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
87
test/serializers/serializer_for_with_namespace_test.rb
Normal file
87
test/serializers/serializer_for_with_namespace_test.rb
Normal file
@ -0,0 +1,87 @@
|
||||
require 'test_helper'
|
||||
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
class SerializerForWithNamespaceTest < ActiveSupport::TestCase
|
||||
class Book < ::Model; end
|
||||
class Page < ::Model; end
|
||||
class Publisher < ::Model; end
|
||||
|
||||
module Api
|
||||
module V3
|
||||
class BookSerializer < ActiveModel::Serializer
|
||||
attributes :title, :author_name
|
||||
|
||||
has_many :pages
|
||||
belongs_to :publisher
|
||||
end
|
||||
|
||||
class PageSerializer < ActiveModel::Serializer
|
||||
attributes :number, :text
|
||||
|
||||
belongs_to :book
|
||||
end
|
||||
|
||||
class PublisherSerializer < ActiveModel::Serializer
|
||||
attributes :name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class BookSerializer < ActiveModel::Serializer
|
||||
attributes :title, :author_name
|
||||
end
|
||||
test 'resource without a namespace' do
|
||||
book = Book.new(title: 'A Post', author_name: 'hello')
|
||||
|
||||
# TODO: this should be able to pull up this serializer without explicitly specifying the serializer
|
||||
# currently, with no options, it still uses the Api::V3 serializer
|
||||
result = ActiveModelSerializers::SerializableResource.new(book, serializer: BookSerializer).serializable_hash
|
||||
|
||||
expected = { title: 'A Post', author_name: 'hello' }
|
||||
assert_equal expected, result
|
||||
end
|
||||
|
||||
test 'resource with namespace' do
|
||||
book = Book.new(title: 'A Post', author_name: 'hi')
|
||||
|
||||
result = ActiveModelSerializers::SerializableResource.new(book, namespace: Api::V3).serializable_hash
|
||||
|
||||
expected = { title: 'A Post', author_name: 'hi', pages: nil, publisher: nil }
|
||||
assert_equal expected, result
|
||||
end
|
||||
|
||||
test 'has_many with nested serializer under the namespace' do
|
||||
page = Page.new(number: 1, text: 'hello')
|
||||
book = Book.new(title: 'A Post', author_name: 'hi', pages: [page])
|
||||
|
||||
result = ActiveModelSerializers::SerializableResource.new(book, namespace: Api::V3).serializable_hash
|
||||
|
||||
expected = {
|
||||
title: 'A Post', author_name: 'hi',
|
||||
publisher: nil,
|
||||
pages: [{
|
||||
number: 1, text: 'hello'
|
||||
}]
|
||||
}
|
||||
assert_equal expected, result
|
||||
end
|
||||
|
||||
test 'belongs_to with nested serializer under the namespace' do
|
||||
publisher = Publisher.new(name: 'Disney')
|
||||
book = Book.new(title: 'A Post', author_name: 'hi', publisher: publisher)
|
||||
|
||||
result = ActiveModelSerializers::SerializableResource.new(book, namespace: Api::V3).serializable_hash
|
||||
|
||||
expected = {
|
||||
title: 'A Post', author_name: 'hi',
|
||||
pages: nil,
|
||||
publisher: {
|
||||
name: 'Disney'
|
||||
}
|
||||
}
|
||||
assert_equal expected, result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue
Block a user