diff --git a/README.textile b/README.textile index 5750cae7..02943ede 100644 --- a/README.textile +++ b/README.textile @@ -456,36 +456,7 @@ The +association_ids+ helper will use the overridden version of the association, this case, +association_ids+ will only include the ids of the comments provided by the +comments+ method. - -h3. Special Association Serializers - -So far, associations defined in serializers use either the +as_json+ method on the model -or the defined serializer for the association type. Sometimes, you may want to serialize -associated models differently when they are requested as part of another resource than -when they are requested on their own. - -For instance, we might want to provide the full comment when it is requested directly, -but only its title when requested as part of the post. To achieve this, you can define -a serializer for associated objects nested inside the main serializer. - -
-class PostSerializer < ActiveModel::Serializer
-  class CommentSerializer < ActiveModel::Serializer
-    attributes :id, :title
-  end
-
-  # same as before
-  # ...
-end
-
- -In other words, if a +PostSerializer+ is trying to serialize comments, it will first -look for +PostSerializer::CommentSerializer+ before falling back to +CommentSerializer+ -and finally +comment.as_json+. - -h3. Overriding the Defaults - -h4. Authorization Scope +h3. Authorization Scope By default, the authorization scope for serializers is +:current_user+. This means that when you call +render json: @post+, the controller will automatically call diff --git a/active_model_serializers.gemspec b/active_model_serializers.gemspec index 81bbe615..42cf876d 100644 --- a/active_model_serializers.gemspec +++ b/active_model_serializers.gemspec @@ -11,9 +11,8 @@ Gem::Specification.new do |gem| gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") gem.name = "active_model_serializers" gem.require_paths = ["lib"] - gem.version = "0.0.1" + gem.version = "0.1.0" gem.add_dependency 'activemodel', '~> 3.0' - gem.add_development_dependency "rails", "~> 3.0" end diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 0a365820..baf6d24b 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -1,7 +1,5 @@ require "active_support/core_ext/class/attribute" -require "active_support/core_ext/string/inflections" require "active_support/core_ext/module/anonymous" -require "set" module ActiveModel # Active Model Array Serializer @@ -84,51 +82,64 @@ module ActiveModel def polymorphic? options[:polymorphic] end + + protected + + def find_serializable(object, scope, context, options) + if serializer + serializer.new(object, scope, options) + elsif object.respond_to?(:active_model_serializer) && (ams = object.active_model_serializer) + ams.new(object, scope, options) + else + object + end + end end class HasMany < Config #:nodoc: - def serialize(collection, scope, options) - collection.map do |item| - serializer.new(item, scope, options).serializable_hash + def serialize(collection, scope, context, options) + array = collection.map do |item| + find_serializable(item, scope, context, options).as_json(:root => false) end + { key => array } end def serialize_ids(collection, scope) - # use named scopes if they are present + # Use pluck or select_columns if available # return collection.ids if collection.respond_to?(:ids) - collection.map do |item| + array = collection.map do |item| item.read_attribute_for_serialization(:id) end + + { key => array } end end class HasOne < Config #:nodoc: - def serialize(object, scope, options) - return unless object - + def serialize(object, scope, context, options) if polymorphic? - polymorphic_type = object.class.to_s.demodulize - serializer_class = "#{object.class.to_s}Serializer".constantize - - serializer_class.new(object, scope, options).serializable_hash.merge({ - "#{name}_type".to_sym => polymorphic_type - }) + if object + find_serializable(object, scope, context, options).as_json(:root => object.class.to_s.demodulize.underscore.to_sym) + else + {} + end else - serializer.new(object, scope, options).serializable_hash + { key => object && find_serializable(object, scope, context, options).as_json(:root => false) } end end + def serialize_ids(object, scope) - return unless object if polymorphic? { - :id => object.read_attribute_for_serialization(:id), - "#{name}_type".to_sym => object.class.to_s.demodulize + object.class.to_s.demodulize.underscore.to_sym => object.read_attribute_for_serialization(:id), } + elsif object + { key => object.read_attribute_for_serialization(:id) } else - object.read_attribute_for_serialization(:id) + { key => nil } end end end @@ -165,12 +176,6 @@ module ActiveModel unless method_defined?(attr) class_eval "def #{attr}() object.#{attr} end", __FILE__, __LINE__ end - - options[:serializer] ||= options[:polymorphic] || begin - serializer_class = (options[:key] || attr).to_s.classify - const_get("#{serializer_class}Serializer") - end - klass.new(attr, options) end end @@ -225,6 +230,9 @@ module ActiveModel # methods, provided by default by ActiveRecord. You can implement these # methods on your custom models if you want the serializer's schema method # to work. + # + # TODO: This is currently coupled to Active Record. We need to + # figure out a way to decouple those two. def schema klass = model_class columns = klass.columns_hash @@ -265,7 +273,6 @@ module ActiveModel def inherited(klass) #:nodoc: return if klass.anonymous? - name = klass.name.demodulize.underscore.sub(/_serializer$/, '') klass.class_eval do @@ -284,8 +291,9 @@ module ActiveModel # Returns a json representation of the serializable # object including the root. - def as_json(*) - if root = @options[:root] || _root + def as_json(options=nil) + options ||= {} + if root = options.fetch(:root, @options.fetch(:root, _root)) @hash = hash = {} hash.merge!(root => serializable_hash) hash @@ -307,6 +315,8 @@ module ActiveModel end end + # Merge associations for embed case by always adding + # root associations to the given hash. def merge_associations(hash, associations) associations.each do |key, value| if hash[key] @@ -324,7 +334,7 @@ module ActiveModel _associations.each do |association| associated_object = send(association.name) - hash[association.key] = association.serialize(associated_object, scope, :hash => @hash) + hash.merge! association.serialize(associated_object, scope, self, :hash => @hash) end hash @@ -337,7 +347,7 @@ module ActiveModel _associations.each do |association| associated_object = send(association.name) - hash[association.key] = association.serialize_ids(associated_object, scope) + hash.merge! association.serialize_ids(associated_object, scope) end hash diff --git a/lib/active_model_serializers.rb b/lib/active_model_serializers.rb index 3019cd99..c13e14a2 100644 --- a/lib/active_model_serializers.rb +++ b/lib/active_model_serializers.rb @@ -1,3 +1,5 @@ +require "active_support" +require "active_support/core_ext/string/inflections" require "active_model" require "active_model/serializer" @@ -5,14 +7,19 @@ module ActiveModel::SerializerSupport extend ActiveSupport::Concern module ClassMethods #:nodoc: - def active_model_serializer - return @active_model_serializer if defined?(@active_model_serializer) + if "".respond_to?(:safe_constantize) + def active_model_serializer + @active_model_serializer ||= "#{self.name}Serializer".safe_constantize + end + else + def active_model_serializer + return @active_model_serializer if defined?(@active_model_serializer) - # Use safe constantize when Rails 3.2 is out - begin - @active_model_serializer = "#{self.name}Serializer".constantize - rescue NameError => e - raise unless e.message =~ /uninitialized constant$/ && e.name.to_s == "#{self.name}Serializer" + begin + @active_model_serializer = "#{self.name}Serializer".constantize + rescue NameError => e + raise unless e.message =~ /uninitialized constant/ + end end end end diff --git a/test/serializer_support_test.rb b/test/serializer_support_test.rb new file mode 100644 index 00000000..b2d2b8be --- /dev/null +++ b/test/serializer_support_test.rb @@ -0,0 +1,11 @@ +require "test_helper" + +class RandomModel + include ActiveModel::SerializerSupport +end + +class SerializerSupportTest < ActiveModel::TestCase + test "it returns nil if no serializer exists" do + assert_equal nil, RandomModel.new.active_model_serializer + end +end \ No newline at end of file diff --git a/test/serializer_test.rb b/test/serializer_test.rb index 1f5e6ef6..d502b226 100644 --- a/test/serializer_test.rb +++ b/test/serializer_test.rb @@ -78,8 +78,13 @@ class SerializerTest < ActiveModel::TestCase { :title => @comment.read_attribute_for_serialization(:title) } end - def as_json(*) - { :comment => serializable_hash } + def as_json(options=nil) + options ||= {} + if options[:root] == false + serializable_hash + else + { :comment => serializable_hash } + end end end @@ -189,65 +194,17 @@ class SerializerTest < ActiveModel::TestCase }, json) end - def test_implicit_serializer - author_serializer = Class.new(ActiveModel::Serializer) do - attributes :first_name - end - - blog_serializer = Class.new(ActiveModel::Serializer) do - const_set(:AuthorSerializer, author_serializer) - has_one :author - end - - user = User.new - blog = Blog.new - blog.author = user - - json = blog_serializer.new(blog, user).as_json - assert_equal({ - :author => { - :first_name => "Jose" - } - }, json) - end - - def test_implicit_serializer_for_has_many - blog_with_posts = Class.new(Blog) do - attr_accessor :posts - end - - blog_serializer = Class.new(ActiveModel::Serializer) do - const_set(:PostSerializer, PostSerializer) - has_many :posts - end - - user = User.new - blog = blog_with_posts.new - blog.posts = [Post.new(:title => 'test')] - - json = blog_serializer.new(blog, user).as_json - assert_equal({ - :posts => [{ - :title => "test", - :body => nil, - :comments => [] - }] - }, json) - end - def test_overridden_associations author_serializer = Class.new(ActiveModel::Serializer) do attributes :first_name end blog_serializer = Class.new(ActiveModel::Serializer) do - const_set(:PersonSerializer, author_serializer) - def person object.author end - has_one :person + has_one :person, :serializer => author_serializer end user = User.new @@ -703,8 +660,7 @@ class SerializerTest < ActiveModel::TestCase serializer = polymorphic_serializer.new(blog, user) assert_equal({ - :writer => { - :writer_type => 'PolymorphicUser', + :polymorphic_user => { :first_name => "Jose", :last_name => "Valim" } @@ -728,10 +684,7 @@ class SerializerTest < ActiveModel::TestCase serializer = polymorphic_serializer.new(blog, user) assert_equal({ - :writer => { - :writer_type => 'PolymorphicUser', - :id => 1 - } + :polymorphic_user => 1 }, serializer.as_json) end @@ -757,7 +710,19 @@ class SerializerTest < ActiveModel::TestCase author = author_class.new(:id => 5, :name => "Tom Dale") post.author = author - hash = serializer_class.new(post, nil, :root => :blog_post) + assert_equal({ + :blog_post => { + :title => "New Post", + :body => "It's a new post!", + :author => { :id => 5, :name => "Tom Dale" } + } + }, serializer_class.new(post, nil, :root => :blog_post).as_json) + + assert_equal({ + :title => "New Post", + :body => "It's a new post!", + :author => { :id => 5, :name => "Tom Dale" } + }, serializer_class.new(post, nil, :root => false).as_json) assert_equal({ :blog_post => { @@ -765,7 +730,13 @@ class SerializerTest < ActiveModel::TestCase :body => "It's a new post!", :author => { :id => 5, :name => "Tom Dale" } } - }, hash.as_json) + }, serializer_class.new(post, nil).as_json(:root => :blog_post)) + + assert_equal({ + :title => "New Post", + :body => "It's a new post!", + :author => { :id => 5, :name => "Tom Dale" } + }, serializer_class.new(post, nil).as_json(:root => false)) end def test_serializer_has_access_to_root_object