Skip to content

Commit

Permalink
This adds namespace lookup to serializer_for (#1968)
Browse files Browse the repository at this point in the history
* 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]
  • Loading branch information
NullVoxPopuli authored Nov 9, 2016
1 parent b709cd4 commit b29395b
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 7 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ cache:

script:
- bundle exec rake ci

after_success:
- codeclimate-test-reporter
env:
global:
- "JRUBY_OPTS='--dev -J-Xmx1024M --debug'"
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
30 changes: 30 additions & 0 deletions docs/general/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 :)
Expand Down
9 changes: 9 additions & 0 deletions lib/action_controller/serialization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ def serialization_scope(scope)
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
Expand All @@ -30,6 +36,9 @@ def get_serializer(resource, options = {})
"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 }
Expand Down
14 changes: 9 additions & 5 deletions lib/active_model/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def self.serializer_for(resource, options = {})
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

Expand All @@ -59,13 +59,14 @@ class << self
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}")

Expand All @@ -84,11 +85,14 @@ def self.serializers_cache
# 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
Expand Down
4 changes: 4 additions & 0 deletions lib/active_model/serializer/reflection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ def value(serializer, include_slice)
#
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)
Expand Down
2 changes: 1 addition & 1 deletion lib/active_model_serializers/serializable_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def serializer
@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)
Expand Down
149 changes: 149 additions & 0 deletions test/action_controller/namespace_lookup_test.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 87 additions & 0 deletions test/serializers/serializer_for_with_namespace_test.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit b29395b

Please sign in to comment.