Skip to content

Controller namespace lookup. #1436

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/action_controller/serialization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def use_adapter?

[:_render_option_json, :_render_with_renderer_json].each do |renderer_method|
define_method renderer_method do |resource, options|
options.fetch(:serialization_context) { options[:serialization_context] = ActiveModelSerializers::SerializationContext.new(request) }
options[:serialization_context] = ActiveModelSerializers::SerializationContext.new(controller: self) unless options.key?(:serialization_context)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see this broken up in to multiple lines

unless options.key?(:serialization_context)
  options[:serialization_context] = ActiveModelSerializers::SerializationContext.new(controller: self)
end

serializable_resource = get_serializer(resource, options)
super(serializable_resource, options)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/active_model/serializable_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,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
38 changes: 26 additions & 12 deletions lib/active_model/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,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[:serialization_context]) }
end
end

Expand All @@ -66,15 +66,27 @@ def self.adapter
end

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

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

chain.push("#{name}::#{serializer_class_name}") if self != ActiveModel::Serializer
chain.push("#{resource_namespace}::#{serializer_class_name}")
# Look for a nested serializer first.
chain.push("::#{name}::#{serializer_class_name}") if self != ActiveModel::Serializer

# Then look for a serializer within the prefix namespaces of the calling controller, if any.
if controller_namespace
controller_chain = controller_namespace.to_s
.split('::')
.reduce([]) { |a, e| a + ["#{a.last}::#{e}"] }
.reverse
.map { |path| "#{path}::#{serializer_class_name}" }
chain.push(*controller_chain)
end

# Finally, look for a serializer in the root namespace.
chain.push("::#{serializer_class_name}")

chain
end
Expand All @@ -91,16 +103,18 @@ 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, serialization_context = nil)
return nil unless config.serializer_lookup_enabled
serializers_cache.fetch_or_store(klass) 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 }
serializers_cache.fetch_or_store([klass, serialization_context]) do
serializer_class = serializer_lookup_chain_for(klass, serialization_context)
.lazy
.map(&:safe_constantize)
.find { |x| x && x < ActiveModel::Serializer }

if serializer_class
serializer_class
elsif klass.superclass
get_serializer_for(klass.superclass)
get_serializer_for(klass.superclass, serialization_context)
end
end
end
Expand Down
13 changes: 9 additions & 4 deletions lib/active_model_serializers/serialization_context.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
module ActiveModelSerializers
class SerializationContext
attr_reader :request_url, :query_parameters
attr_reader :request_url, :query_parameters, :controller_namespace

def initialize(request)
@request_url = request.original_url[/\A[^?]+/]
@query_parameters = request.query_parameters
def initialize(options = {})
self.controller = options[:controller] if options.key?(:controller)
end

def controller=(controller)
@request_url = controller.request.original_url[/\A[^?]+/]
@query_parameters = controller.request.query_parameters
@controller_namespace = controller.class.to_s.deconstantize
end
end
end
23 changes: 9 additions & 14 deletions test/active_model_serializers/serialization_context_test.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
require 'test_helper'

class ActiveModelSerializers::SerializationContextTest < ActiveSupport::TestCase
def create_context
request = Minitest::Mock.new
request.expect(:original_url, 'original_url')
request.expect(:query_parameters, 'query_parameters')

ActiveModelSerializers::SerializationContext.new(request)
end

def test_create_context_with_request_url_and_query_parameters
context = create_context

assert_equal context.request_url, 'original_url'
assert_equal context.query_parameters, 'query_parameters'
module ActiveModelSerializers
class SerializationContextTest < ActionController::TestCase
def test_create_context_with_request_url_and_query_parameters
context = ActiveModelSerializers::SerializationContext.new(controller: self)

assert_equal('http://test.host', context.request_url)
assert_equal({}, context.query_parameters)
assert_equal('ActiveModelSerializers', context.controller_namespace)
end
end
end
10 changes: 4 additions & 6 deletions test/serializers/associations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,7 @@ def test_associations_namespaced_resources

class NestedSerializersTest < ActiveSupport::TestCase
Post = Class.new(::Model)
Comment = Class.new(::Model)
Author = Class.new(::Model)
Description = Class.new(::Model)
::Description = Class.new(::Model)
class PostSerializer < ActiveModel::Serializer
has_many :comments
CommentSerializer = Class.new(ActiveModel::Serializer)
Expand All @@ -215,9 +213,9 @@ class PostSerializer < ActiveModel::Serializer
end

def setup
@comment = Comment.new
@author = Author.new
@description = Description.new
@comment = ::Comment.new
@author = ::Author.new
@description = ::Description.new
@post = Post.new(comments: [@comment],
author: @author,
description: @description)
Expand Down
48 changes: 42 additions & 6 deletions test/serializers/serializer_for_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,19 +115,55 @@ def test_serializer_for_namespaced_resource_with_lookup_disabled
assert_equal nil, serializer
end

def test_serializer_for_nested_resource
comment = ResourceNamespace::Comment.new
serializer = ResourceNamespace::PostSerializer.serializer_for(comment)
assert_equal ResourceNamespace::PostSerializer::CommentSerializer, serializer
end

def test_serializer_for_nested_resource_with_lookup_disabled
comment = ResourceNamespace::Comment.new
serializer = with_serializer_lookup_disabled do
ResourceNamespace::PostSerializer.serializer_for(comment)
end
assert_equal nil, serializer
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test did not make sense anymore with the new lookup convention.

end

module ::ResourceNamespace
class Resource; end
end

def test_serializer_lookup_chain
chain = ActiveModel::Serializer.serializer_lookup_chain_for(::ResourceNamespace::Resource)
expected = ['::ResourceNamespace::ResourceSerializer']
assert_equal(expected, chain)
end

def test_serializer_lookup_chain_nested
chain = ::PostSerializer.serializer_lookup_chain_for(::ResourceNamespace::Resource)
expected = ['::PostSerializer::ResourceNamespace::ResourceSerializer',
'::ResourceNamespace::ResourceSerializer']
assert_equal(expected, chain)
end

def test_serializer_lookup_chain_controller
context = ActiveModelSerializers::SerializationContext.new
context.instance_variable_set(:@controller_namespace, 'Api::V1')
assert_equal('Api::V1', context.controller_namespace)

chain = ActiveModel::Serializer.serializer_lookup_chain_for(::ResourceNamespace::Resource, context)
expected = ['::Api::V1::ResourceNamespace::ResourceSerializer',
'::Api::ResourceNamespace::ResourceSerializer',
'::ResourceNamespace::ResourceSerializer']
assert_equal(expected, chain)
end

def test_serializer_lookup_chain_controller_nested
context = ActiveModelSerializers::SerializationContext.new
context.instance_variable_set(:@controller_namespace, 'Api::V1')
assert_equal('Api::V1', context.controller_namespace)

chain = ::PostSerializer.serializer_lookup_chain_for(::ResourceNamespace::Resource, context)
expected = ['::PostSerializer::ResourceNamespace::ResourceSerializer',
'::Api::V1::ResourceNamespace::ResourceSerializer',
'::Api::ResourceNamespace::ResourceSerializer',
'::ResourceNamespace::ResourceSerializer']
assert_equal(expected, chain)
end
end
end
end
Expand Down