diff --git a/CHANGELOG.md b/CHANGELOG.md index f870df96..5692fe54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ * Require rails >= 3.2. +* Serializers for associations are being looked up in a parent serializer's namespace first. Same with controllers' namespaces. + +* Added a "prefix" option in case you want to use a different version of serializer. + # VERSION 0.8.1 * Fix bug whereby a serializer using 'options' would blow up. diff --git a/lib/action_controller/serialization.rb b/lib/action_controller/serialization.rb index 5b1ee4ee..d86b2ac0 100644 --- a/lib/action_controller/serialization.rb +++ b/lib/action_controller/serialization.rb @@ -57,6 +57,18 @@ module ActionController private + def namespace_for_serializer + @namespace_for_serializer ||= self.class.parent unless self.class.parent == Object + end + + def default_serializer(resource) + options = {}.tap do |o| + o[:namespace] = namespace_for_serializer if namespace_for_serializer + end + + ActiveModel::Serializer.serializer_for(resource, options) + end + def default_serializer_options {} end @@ -69,9 +81,13 @@ module ActionController def build_json_serializer(resource, options = {}) options = default_serializer_options.merge(options) - if serializer = options.fetch(:serializer, ActiveModel::Serializer.serializer_for(resource)) + if serializer = options.fetch(:serializer, default_serializer(resource)) options[:scope] = serialization_scope unless options.has_key?(:scope) - options[:resource_name] = controller_name if resource.respond_to?(:to_ary) + + if resource.respond_to?(:to_ary) + options[:resource_name] = controller_name + options[:namespace] = namespace_for_serializer if namespace_for_serializer + end serializer.new(resource, options) end diff --git a/lib/active_model/array_serializer.rb b/lib/active_model/array_serializer.rb index fe985e8b..e0c3ecfc 100644 --- a/lib/active_model/array_serializer.rb +++ b/lib/active_model/array_serializer.rb @@ -22,6 +22,7 @@ module ActiveModel @resource_name = options[:resource_name] @only = options[:only] ? Array(options[:only]) : nil @except = options[:except] ? Array(options[:except]) : nil + @namespace = options[:namespace] @key_format = options[:key_format] || options[:each_serializer].try(:key_format) end attr_accessor :object, :scope, :root, :meta_key, :meta, :key_format @@ -33,13 +34,13 @@ module ActiveModel end def serializer_for(item) - serializer_class = @each_serializer || Serializer.serializer_for(item) || DefaultSerializer + serializer_class = @each_serializer || Serializer.serializer_for(item, namespace: @namespace) || DefaultSerializer serializer_class.new(item, scope: scope, key_format: key_format, only: @only, except: @except, polymorphic: @polymorphic) end def serializable_object @object.map do |item| - serializer_for(item).serializable_object + serializer_for(item).serializable_object_with_notification end end alias_method :serializable_array, :serializable_object @@ -59,6 +60,7 @@ module ActiveModel end private + def instrumentation_keys [:object, :scope, :root, :meta_key, :meta, :each_serializer, :resource_name, :key_format] end diff --git a/lib/active_model/serializable.rb b/lib/active_model/serializable.rb index e70a9afc..9a33d397 100644 --- a/lib/active_model/serializable.rb +++ b/lib/active_model/serializable.rb @@ -1,5 +1,11 @@ +require 'active_model/serializable/utils' + module ActiveModel module Serializable + def self.included(base) + base.extend Utils + end + def as_json(options={}) instrument('!serialize') do if root = options.fetch(:root, json_key) @@ -12,6 +18,12 @@ module ActiveModel end end + def serializable_object_with_notification + instrument('!serialize') do + serializable_object + end + end + def serializable_data embedded_in_root_associations.tap do |hash| if respond_to?(:meta) && meta @@ -20,11 +32,21 @@ module ActiveModel end end + def namespace + get_namespace && Utils._const_get(get_namespace) + end + def embedded_in_root_associations {} end private + + def get_namespace + modules = self.class.name.split('::') + modules[0..-2].join('::') if modules.size > 1 + end + def instrument(action, &block) payload = instrumentation_keys.inject({ serializer: self.class.name }) do |payload, key| payload[:payload] = self.instance_variable_get(:"@#{key}") diff --git a/lib/active_model/serializable/utils.rb b/lib/active_model/serializable/utils.rb new file mode 100644 index 00000000..7471bd0f --- /dev/null +++ b/lib/active_model/serializable/utils.rb @@ -0,0 +1,12 @@ +module ActiveModel + module Serializable + module Utils + extend self + + def _const_get(const) + method = RUBY_VERSION >= '2.0' ? :const_get : :qualified_const_get + Object.send method, const + end + end + end +end \ No newline at end of file diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index bb7ef131..689e132a 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -55,32 +55,18 @@ end end attr_reader :key_format - if RUBY_VERSION >= '2.0' - def serializer_for(resource) - if resource.respond_to?(:to_ary) - if Object.constants.include?(:ArraySerializer) - ::ArraySerializer - else - ArraySerializer - end + def serializer_for(resource, options = {}) + if resource.respond_to?(:to_ary) + if Object.constants.include?(:ArraySerializer) + ::ArraySerializer else - begin - Object.const_get "#{resource.class.name}Serializer" - rescue NameError - nil - end + ArraySerializer end - end - else - def serializer_for(resource) - if resource.respond_to?(:to_ary) - if Object.constants.include?(:ArraySerializer) - ::ArraySerializer - else - ArraySerializer - end - else - "#{resource.class.name}Serializer".safe_constantize + else + begin + _const_get build_serializer_class(resource, options) + rescue NameError + nil end end end @@ -113,6 +99,14 @@ end private + def build_serializer_class(resource, options) + "".tap do |klass_name| + klass_name << "#{options[:namespace]}::" if options[:namespace] + klass_name << options[:prefix].to_s.classify if options[:prefix] + klass_name << "#{resource.class.name}Serializer" + end + end + def associate(klass, *attrs) options = attrs.extract_options! @@ -219,7 +213,16 @@ end def build_serializer(association) object = send(association.name) - association.build_serializer(object, scope: scope) + association.build_serializer(object, association_options_for_serializer(association)) + end + + def association_options_for_serializer(association) + prefix = association.options[:prefix] + + { scope: scope }.tap do |opts| + opts[:namespace] = namespace if namespace + opts[:prefix] = prefix if prefix + end end def serialize(association) diff --git a/lib/active_model/serializer/association.rb b/lib/active_model/serializer/association.rb index f62c982f..5ea22056 100644 --- a/lib/active_model/serializer/association.rb +++ b/lib/active_model/serializer/association.rb @@ -42,8 +42,8 @@ module ActiveModel @embed_objects = embed == :object || embed == :objects end - def serializer_from_object(object) - Serializer.serializer_for(object) + def serializer_from_object(object, options = {}) + Serializer.serializer_for(object, options) end def default_serializer @@ -51,7 +51,7 @@ module ActiveModel end def build_serializer(object, options = {}) - serializer_class(object).new(object, options.merge(self.options)) + serializer_class(object, options).new(object, options.merge(self.options)) end end end diff --git a/lib/active_model/serializer/association/has_many.rb b/lib/active_model/serializer/association/has_many.rb index 4480afc6..d1b8d8eb 100644 --- a/lib/active_model/serializer/association/has_many.rb +++ b/lib/active_model/serializer/association/has_many.rb @@ -8,7 +8,7 @@ module ActiveModel @key ||= "#{name.to_s.singularize}_ids" end - def serializer_class(object) + def serializer_class(object, _) if use_array_serializer? ArraySerializer else diff --git a/lib/active_model/serializer/association/has_one.rb b/lib/active_model/serializer/association/has_one.rb index 4965e428..3b9acddf 100644 --- a/lib/active_model/serializer/association/has_one.rb +++ b/lib/active_model/serializer/association/has_one.rb @@ -8,8 +8,8 @@ module ActiveModel @key ||= "#{name}_id" end - def serializer_class(object) - serializer_from_options || serializer_from_object(object) || default_serializer + def serializer_class(object, options = {}) + serializer_from_options || serializer_from_object(object, options) || default_serializer end def build_serializer(object, options = {}) diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb index 17011ba7..14fa3e5a 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -135,3 +135,10 @@ end class VideoSerializer < ActiveModel::Serializer attributes :html end + +class ShortProfileSerializer < ::ProfileSerializer; end + +module TestNamespace + class ProfileSerializer < ::ProfileSerializer; end + class UserSerializer < ::UserSerializer; end +end diff --git a/test/integration/action_controller/namespaced_serialization_test.rb b/test/integration/action_controller/namespaced_serialization_test.rb new file mode 100644 index 00000000..4a5fbbf9 --- /dev/null +++ b/test/integration/action_controller/namespaced_serialization_test.rb @@ -0,0 +1,47 @@ +require 'test_helper' + +module ActionController + module Serialization + class NamespacedSerializationTest < ActionController::TestCase + class TestNamespace::MyController < ActionController::Base + def render_profile_with_namespace + render json: Profile.new({ name: 'Name 1', description: 'Description 1'}) + end + + def render_profiles_with_namespace + render json: [Profile.new({ name: 'Name 1', description: 'Description 1'})] + end + + def render_comment + render json: Comment.new(content: 'Comment 1') + end + + def render_comments + render json: [Comment.new(content: 'Comment 1')] + end + end + + tests TestNamespace::MyController + + def test_render_profile_with_namespace + get :render_profile_with_namespace + assert_serializer TestNamespace::ProfileSerializer + end + + def test_render_profiles_with_namespace + get :render_profiles_with_namespace + assert_serializer TestNamespace::ProfileSerializer + end + + def test_fallback_to_a_version_without_namespace + get :render_comment + assert_serializer CommentSerializer + end + + def test_array_fallback_to_a_version_without_namespace + get :render_comments + assert_serializer CommentSerializer + end + end + end +end \ No newline at end of file diff --git a/test/unit/active_model/serializer/associations/build_serializer_test.rb b/test/unit/active_model/serializer/associations/build_serializer_test.rb index 2848badf..414a400f 100644 --- a/test/unit/active_model/serializer/associations/build_serializer_test.rb +++ b/test/unit/active_model/serializer/associations/build_serializer_test.rb @@ -7,6 +7,7 @@ module ActiveModel def setup @association = Association::HasOne.new('post', serializer: PostSerializer) @post = Post.new({ title: 'Title 1', body: 'Body 1', date: '1/1/2000' }) + @user = User.new end def test_build_serializer_for_array_called_twice @@ -15,6 +16,20 @@ module ActiveModel assert_instance_of(PostSerializer, serializer) end end + + def test_build_serializer_from_in_a_namespace + assoc = Association::HasOne.new('profile') + serializer = TestNamespace::UserSerializer.new(@user).build_serializer(assoc) + + assert_instance_of(TestNamespace::ProfileSerializer, serializer) + end + + def test_build_serializer_with_prefix + assoc = Association::HasOne.new('profile', prefix: :short) + serializer = UserSerializer.new(@user).build_serializer(assoc) + + assert_instance_of(ShortProfileSerializer, serializer) + end end end end