Skip to content

Commit b29395b

Browse files
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]
1 parent b709cd4 commit b29395b

File tree

9 files changed

+295
-7
lines changed

9 files changed

+295
-7
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ cache:
2020

2121
script:
2222
- bundle exec rake ci
23-
23+
after_success:
24+
- codeclimate-test-reporter
2425
env:
2526
global:
2627
- "JRUBY_OPTS='--dev -J-Xmx1024M --debug'"

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Fixes:
1313

1414
Features:
1515

16+
- [#1968](https://github.com/rails-api/active_model_serializers/pull/1968) (@NullVoxPopuli)
17+
- Add controller namespace to default controller lookup
18+
- Provide a `namespace` render option
19+
- document how set the namespace in the controller for implicit lookup.
1620
- [#1791](https://github.com/rails-api/active_model_serializers/pull/1791) (@bf4, @youroff, @NullVoxPopuli)
1721
- Added `jsonapi_namespace_separator` config option.
1822
- [#1889](https://github.com/rails-api/active_model_serializers/pull/1889) Support key transformation for Attributes adapter (@iancanderson, @danbee)

docs/general/rendering.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,36 @@ This will be rendered as:
243243
```
244244
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.
245245

246+
#### namespace
247+
248+
The namespace for serializer lookup is based on the controller.
249+
250+
To configure the implicit namespace, in your controller, create a before filter
251+
252+
```ruby
253+
before_action do
254+
self.namespace_for_serializer = Api::V2
255+
end
256+
```
257+
258+
`namespace` can also be passed in as a render option:
259+
260+
261+
```ruby
262+
@post = Post.first
263+
render json: @post, namespace: Api::V2
264+
```
265+
266+
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.
267+
268+
The `namespace` can be any object whose namespace can be represented by string interpolation (i.e. by calling to_s)
269+
- Module `Api::V2`
270+
- String `'Api::V2'`
271+
- Symbol `:'Api::V2'`
272+
273+
Note that by using a string and symbol, Ruby will assume the namespace is defined at the top level.
274+
275+
246276
#### serializer
247277

248278
PR please :)

lib/action_controller/serialization.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ def serialization_scope(scope)
1616
included do
1717
class_attribute :_serialization_scope
1818
self._serialization_scope = :current_user
19+
20+
attr_writer :namespace_for_serializer
21+
end
22+
23+
def namespace_for_serializer
24+
@namespace_for_serializer ||= self.class.parent unless self.class.parent == Object
1925
end
2026

2127
def serialization_scope
@@ -30,6 +36,9 @@ def get_serializer(resource, options = {})
3036
"Please pass 'adapter: false' or see ActiveSupport::SerializableResource.new"
3137
options[:adapter] = false
3238
end
39+
40+
options.fetch(:namespace) { options[:namespace] = namespace_for_serializer }
41+
3342
serializable_resource = ActiveModelSerializers::SerializableResource.new(resource, options)
3443
serializable_resource.serialization_scope ||= options.fetch(:scope) { serialization_scope }
3544
serializable_resource.serialization_scope_name = options.fetch(:scope_name) { _serialization_scope }

lib/active_model/serializer.rb

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def self.serializer_for(resource, options = {})
4444
elsif resource.respond_to?(:to_ary)
4545
config.collection_serializer
4646
else
47-
options.fetch(:serializer) { get_serializer_for(resource.class) }
47+
options.fetch(:serializer) { get_serializer_for(resource.class, options[:namespace]) }
4848
end
4949
end
5050

@@ -59,13 +59,14 @@ class << self
5959
end
6060

6161
# @api private
62-
def self.serializer_lookup_chain_for(klass)
62+
def self.serializer_lookup_chain_for(klass, namespace = nil)
6363
chain = []
6464

6565
resource_class_name = klass.name.demodulize
6666
resource_namespace = klass.name.deconstantize
6767
serializer_class_name = "#{resource_class_name}Serializer"
6868

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

@@ -84,11 +85,14 @@ def self.serializers_cache
8485
# 1. class name appended with "Serializer"
8586
# 2. try again with superclass, if present
8687
# 3. nil
87-
def self.get_serializer_for(klass)
88+
def self.get_serializer_for(klass, namespace = nil)
8889
return nil unless config.serializer_lookup_enabled
89-
serializers_cache.fetch_or_store(klass) do
90+
91+
cache_key = ActiveSupport::Cache.expand_cache_key(klass, namespace)
92+
serializers_cache.fetch_or_store(cache_key) do
9093
# NOTE(beauby): When we drop 1.9.3 support we can lazify the map for perfs.
91-
serializer_class = serializer_lookup_chain_for(klass).map(&:safe_constantize).find { |x| x && x < ActiveModel::Serializer }
94+
lookup_chain = serializer_lookup_chain_for(klass, namespace)
95+
serializer_class = lookup_chain.map(&:safe_constantize).find { |x| x && x < ActiveModel::Serializer }
9296

9397
if serializer_class
9498
serializer_class

lib/active_model/serializer/reflection.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ def value(serializer, include_slice)
106106
#
107107
def build_association(parent_serializer, parent_serializer_options, include_slice = {})
108108
reflection_options = options.dup
109+
110+
# Pass the parent's namespace onto the child serializer
111+
reflection_options[:namespace] ||= parent_serializer_options[:namespace]
112+
109113
association_value = value(parent_serializer, include_slice)
110114
serializer_class = parent_serializer.class.serializer_for(association_value, reflection_options)
111115
reflection_options[:include_data] = include_data?(include_slice)

lib/active_model_serializers/serializable_resource.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def serializer
5555
@serializer ||=
5656
begin
5757
@serializer = serializer_opts.delete(:serializer)
58-
@serializer ||= ActiveModel::Serializer.serializer_for(resource)
58+
@serializer ||= ActiveModel::Serializer.serializer_for(resource, serializer_opts)
5959

6060
if serializer_opts.key?(:each_serializer)
6161
serializer_opts[:serializer] = serializer_opts.delete(:each_serializer)
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
require 'test_helper'
2+
3+
module ActionController
4+
module Serialization
5+
class NamespaceLookupTest < ActionController::TestCase
6+
class Book < ::Model; end
7+
class Page < ::Model; end
8+
class Writer < ::Model; end
9+
10+
module Api
11+
module V2
12+
class BookSerializer < ActiveModel::Serializer
13+
attributes :title
14+
end
15+
end
16+
17+
module V3
18+
class BookSerializer < ActiveModel::Serializer
19+
attributes :title, :body
20+
21+
belongs_to :writer
22+
end
23+
24+
class WriterSerializer < ActiveModel::Serializer
25+
attributes :name
26+
end
27+
28+
class LookupTestController < ActionController::Base
29+
before_action only: [:namespace_set_in_before_filter] do
30+
self.namespace_for_serializer = Api::V2
31+
end
32+
33+
def implicit_namespaced_serializer
34+
writer = Writer.new(name: 'Bob')
35+
book = Book.new(title: 'New Post', body: 'Body', writer: writer)
36+
37+
render json: book
38+
end
39+
40+
def explicit_namespace_as_module
41+
book = Book.new(title: 'New Post', body: 'Body')
42+
43+
render json: book, namespace: Api::V2
44+
end
45+
46+
def explicit_namespace_as_string
47+
book = Book.new(title: 'New Post', body: 'Body')
48+
49+
# because this is a string, ruby can't auto-lookup the constant, so otherwise
50+
# the looku things we mean ::Api::V2
51+
render json: book, namespace: 'ActionController::Serialization::NamespaceLookupTest::Api::V2'
52+
end
53+
54+
def explicit_namespace_as_symbol
55+
book = Book.new(title: 'New Post', body: 'Body')
56+
57+
# because this is a string, ruby can't auto-lookup the constant, so otherwise
58+
# the looku things we mean ::Api::V2
59+
render json: book, namespace: :'ActionController::Serialization::NamespaceLookupTest::Api::V2'
60+
end
61+
62+
def invalid_namespace
63+
book = Book.new(title: 'New Post', body: 'Body')
64+
65+
render json: book, namespace: :api_v2
66+
end
67+
68+
def namespace_set_in_before_filter
69+
book = Book.new(title: 'New Post', body: 'Body')
70+
render json: book
71+
end
72+
end
73+
end
74+
end
75+
76+
tests Api::V3::LookupTestController
77+
78+
setup do
79+
@test_namespace = self.class.parent
80+
end
81+
82+
test 'implicitly uses namespaced serializer' do
83+
get :implicit_namespaced_serializer
84+
85+
assert_serializer Api::V3::BookSerializer
86+
87+
expected = { 'title' => 'New Post', 'body' => 'Body', 'writer' => { 'name' => 'Bob' } }
88+
actual = JSON.parse(@response.body)
89+
90+
assert_equal expected, actual
91+
end
92+
93+
test 'explicit namespace as module' do
94+
get :explicit_namespace_as_module
95+
96+
assert_serializer Api::V2::BookSerializer
97+
98+
expected = { 'title' => 'New Post' }
99+
actual = JSON.parse(@response.body)
100+
101+
assert_equal expected, actual
102+
end
103+
104+
test 'explicit namespace as string' do
105+
get :explicit_namespace_as_string
106+
107+
assert_serializer Api::V2::BookSerializer
108+
109+
expected = { 'title' => 'New Post' }
110+
actual = JSON.parse(@response.body)
111+
112+
assert_equal expected, actual
113+
end
114+
115+
test 'explicit namespace as symbol' do
116+
get :explicit_namespace_as_symbol
117+
118+
assert_serializer Api::V2::BookSerializer
119+
120+
expected = { 'title' => 'New Post' }
121+
actual = JSON.parse(@response.body)
122+
123+
assert_equal expected, actual
124+
end
125+
126+
test 'invalid namespace' do
127+
get :invalid_namespace
128+
129+
assert_serializer ActiveModel::Serializer::Null
130+
131+
expected = { 'title' => 'New Post', 'body' => 'Body' }
132+
actual = JSON.parse(@response.body)
133+
134+
assert_equal expected, actual
135+
end
136+
137+
test 'namespace set in before filter' do
138+
get :namespace_set_in_before_filter
139+
140+
assert_serializer Api::V2::BookSerializer
141+
142+
expected = { 'title' => 'New Post' }
143+
actual = JSON.parse(@response.body)
144+
145+
assert_equal expected, actual
146+
end
147+
end
148+
end
149+
end
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
require 'test_helper'
2+
3+
module ActiveModel
4+
class Serializer
5+
class SerializerForWithNamespaceTest < ActiveSupport::TestCase
6+
class Book < ::Model; end
7+
class Page < ::Model; end
8+
class Publisher < ::Model; end
9+
10+
module Api
11+
module V3
12+
class BookSerializer < ActiveModel::Serializer
13+
attributes :title, :author_name
14+
15+
has_many :pages
16+
belongs_to :publisher
17+
end
18+
19+
class PageSerializer < ActiveModel::Serializer
20+
attributes :number, :text
21+
22+
belongs_to :book
23+
end
24+
25+
class PublisherSerializer < ActiveModel::Serializer
26+
attributes :name
27+
end
28+
end
29+
end
30+
31+
class BookSerializer < ActiveModel::Serializer
32+
attributes :title, :author_name
33+
end
34+
test 'resource without a namespace' do
35+
book = Book.new(title: 'A Post', author_name: 'hello')
36+
37+
# TODO: this should be able to pull up this serializer without explicitly specifying the serializer
38+
# currently, with no options, it still uses the Api::V3 serializer
39+
result = ActiveModelSerializers::SerializableResource.new(book, serializer: BookSerializer).serializable_hash
40+
41+
expected = { title: 'A Post', author_name: 'hello' }
42+
assert_equal expected, result
43+
end
44+
45+
test 'resource with namespace' do
46+
book = Book.new(title: 'A Post', author_name: 'hi')
47+
48+
result = ActiveModelSerializers::SerializableResource.new(book, namespace: Api::V3).serializable_hash
49+
50+
expected = { title: 'A Post', author_name: 'hi', pages: nil, publisher: nil }
51+
assert_equal expected, result
52+
end
53+
54+
test 'has_many with nested serializer under the namespace' do
55+
page = Page.new(number: 1, text: 'hello')
56+
book = Book.new(title: 'A Post', author_name: 'hi', pages: [page])
57+
58+
result = ActiveModelSerializers::SerializableResource.new(book, namespace: Api::V3).serializable_hash
59+
60+
expected = {
61+
title: 'A Post', author_name: 'hi',
62+
publisher: nil,
63+
pages: [{
64+
number: 1, text: 'hello'
65+
}]
66+
}
67+
assert_equal expected, result
68+
end
69+
70+
test 'belongs_to with nested serializer under the namespace' do
71+
publisher = Publisher.new(name: 'Disney')
72+
book = Book.new(title: 'A Post', author_name: 'hi', publisher: publisher)
73+
74+
result = ActiveModelSerializers::SerializableResource.new(book, namespace: Api::V3).serializable_hash
75+
76+
expected = {
77+
title: 'A Post', author_name: 'hi',
78+
pages: nil,
79+
publisher: {
80+
name: 'Disney'
81+
}
82+
}
83+
assert_equal expected, result
84+
end
85+
end
86+
end
87+
end

0 commit comments

Comments
 (0)