From 26c0847563371685539cb584558067ae9299f733 Mon Sep 17 00:00:00 2001 From: Theodore Konukhov Date: Sun, 24 Aug 2014 15:57:24 +0200 Subject: [PATCH 1/6] - If a controller is in a namespace, serializer will be looked up in this namespace first. - Serializers for associations will be looked up in a parent serializer's namespace first too. - "Prefix" option for different versions of associations serializers. Example: has_many :users, as: :short -> ShortUserSerializer TODO: tests, config option for enabling namespace lookup --- lib/action_controller/serialization.rb | 20 +++++++++++++++++-- lib/active_model/array_serializer.rb | 4 +++- lib/active_model/serializable.rb | 16 +++++++++++++++ lib/active_model/serializer.rb | 27 +++++++++++++++++++++----- 4 files changed, 59 insertions(+), 8 deletions(-) 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 8c050770..25093ace 100644 --- a/lib/active_model/array_serializer.rb +++ b/lib/active_model/array_serializer.rb @@ -21,6 +21,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 @@ -32,7 +33,7 @@ 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) end @@ -58,6 +59,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..1faf62f1 100644 --- a/lib/active_model/serializable.rb +++ b/lib/active_model/serializable.rb @@ -20,11 +20,27 @@ module ActiveModel end end + if RUBY_VERSION >= '2.0' + def namespace + get_namespace && Object.const_get(get_namespace) + end + else + def namespace + get_namespace && get_namespace.safe_constantize + end + 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/serializer.rb b/lib/active_model/serializer.rb index e9edf41a..3913a5c3 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -56,7 +56,7 @@ end attr_reader :key_format if RUBY_VERSION >= '2.0' - def serializer_for(resource) + def serializer_for(resource, options = {}) if resource.respond_to?(:to_ary) if Object.constants.include?(:ArraySerializer) ::ArraySerializer @@ -65,14 +65,14 @@ end end else begin - Object.const_get "#{resource.class.name}Serializer" + Object.const_get build_serializer_class(resource, options) rescue NameError nil end end end else - def serializer_for(resource) + def serializer_for(resource, options = {}) if resource.respond_to?(:to_ary) if Object.constants.include?(:ArraySerializer) ::ArraySerializer @@ -80,7 +80,7 @@ end ArraySerializer end else - "#{resource.class.name}Serializer".safe_constantize + build_serializer_class(resource, options).safe_constantize end end end @@ -113,6 +113,14 @@ end private + def build_serializer_class(resource, options) + "".tap do |klass_name| + klass_name << "#{options[:namespace]}::" if options[:namespace] + klass_name << prefix.to_s.classify if options[:prefix] + klass_name << "#{resource.class.name}Serializer" + end + end + def associate(klass, *attrs) options = attrs.extract_options! @@ -215,7 +223,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) From dde14929343b0e7bb721bfce186a8be427aae26b Mon Sep 17 00:00:00 2001 From: Theodore Konukhov Date: Tue, 26 Aug 2014 18:07:31 +0200 Subject: [PATCH 2/6] tests for namespaced controlleler --- lib/active_model/array_serializer.rb | 2 +- lib/active_model/serializable.rb | 6 +++ test/fixtures/poro.rb | 9 ++++ .../namespaced_serialization_test.rb | 47 +++++++++++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 test/integration/action_controller/namespaced_serialization_test.rb diff --git a/lib/active_model/array_serializer.rb b/lib/active_model/array_serializer.rb index 25093ace..9da8194a 100644 --- a/lib/active_model/array_serializer.rb +++ b/lib/active_model/array_serializer.rb @@ -39,7 +39,7 @@ module ActiveModel 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 diff --git a/lib/active_model/serializable.rb b/lib/active_model/serializable.rb index 1faf62f1..4eb6b9f4 100644 --- a/lib/active_model/serializable.rb +++ b/lib/active_model/serializable.rb @@ -12,6 +12,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 diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb index a357c24e..de3c2037 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -73,3 +73,12 @@ end class WebLogLowerCamelSerializer < WebLogSerializer format_keys :lower_camel end + +class ShortUserSerializer < ActiveModel::Serializer + attributes :name +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 From b297f17e532411db95bdf93469b38a07bcd155d7 Mon Sep 17 00:00:00 2001 From: Theodore Konukhov Date: Fri, 29 Aug 2014 06:11:40 +0200 Subject: [PATCH 3/6] test for namespaced associations + bug fixed --- lib/active_model/serializer.rb | 2 +- lib/active_model/serializer/associations.rb | 12 ++++++------ test/fixtures/poro.rb | 4 +--- .../associations/build_serializer_test.rb | 15 +++++++++++++++ 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 3913a5c3..48a34bf0 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -116,7 +116,7 @@ end def build_serializer_class(resource, options) "".tap do |klass_name| klass_name << "#{options[:namespace]}::" if options[:namespace] - klass_name << prefix.to_s.classify if options[:prefix] + klass_name << options[:prefix].to_s.classify if options[:prefix] klass_name << "#{resource.class.name}Serializer" end end diff --git a/lib/active_model/serializer/associations.rb b/lib/active_model/serializer/associations.rb index 44d7c84f..da6271a1 100644 --- a/lib/active_model/serializer/associations.rb +++ b/lib/active_model/serializer/associations.rb @@ -38,8 +38,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 @@ -47,7 +47,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 class HasOne < Association @@ -57,8 +57,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 = {}) @@ -74,7 +74,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/test/fixtures/poro.rb b/test/fixtures/poro.rb index de3c2037..5ad9dd82 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -74,9 +74,7 @@ class WebLogLowerCamelSerializer < WebLogSerializer format_keys :lower_camel end -class ShortUserSerializer < ActiveModel::Serializer - attributes :name -end +class ShortProfileSerializer < ::ProfileSerializer; end module TestNamespace class ProfileSerializer < ::ProfileSerializer; end 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 From e3bd56f73ad0d5ab76e69372faac1b590cb271a8 Mon Sep 17 00:00:00 2001 From: Theodore Konukhov Date: Fri, 29 Aug 2014 06:23:54 +0200 Subject: [PATCH 4/6] changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) 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. From 095bae31d43c9901bf31f01ae4a03dd6eb1eb408 Mon Sep 17 00:00:00 2001 From: Theodore Konukhov Date: Fri, 29 Aug 2014 19:38:53 +0200 Subject: [PATCH 5/6] fix for ruby 1.9. --- lib/active_model/serializable.rb | 16 ++++++------ lib/active_model/serializable/utils.rb | 12 +++++++++ lib/active_model/serializer.rb | 34 ++++++++------------------ 3 files changed, 30 insertions(+), 32 deletions(-) create mode 100644 lib/active_model/serializable/utils.rb diff --git a/lib/active_model/serializable.rb b/lib/active_model/serializable.rb index 4eb6b9f4..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) @@ -26,14 +32,8 @@ module ActiveModel end end - if RUBY_VERSION >= '2.0' - def namespace - get_namespace && Object.const_get(get_namespace) - end - else - def namespace - get_namespace && get_namespace.safe_constantize - end + def namespace + get_namespace && Utils._const_get(get_namespace) end def embedded_in_root_associations 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 da009ef2..52894f0c 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, options = {}) - 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 build_serializer_class(resource, options) - rescue NameError - nil - end + ArraySerializer end - end - else - def serializer_for(resource, options = {}) - if resource.respond_to?(:to_ary) - if Object.constants.include?(:ArraySerializer) - ::ArraySerializer - else - ArraySerializer - end - else - build_serializer_class(resource, options).safe_constantize + else + begin + _const_get build_serializer_class(resource, options) + rescue NameError + nil end end end From 0292940ab41662d14355c92c368f8bb30c455988 Mon Sep 17 00:00:00 2001 From: Theodore Konukhov Date: Fri, 29 Aug 2014 19:51:32 +0200 Subject: [PATCH 6/6] typo --- lib/active_model/serializer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 52894f0c..61e67869 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -102,7 +102,7 @@ end 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 << options[:prefix].to_s.classify if options[:prefix] klass_name << "#{resource.class.name}Serializer" end end