From 5f198667bec0cdbe85ffd61eb2e649171e2b6750 Mon Sep 17 00:00:00 2001 From: Guillermo Iguaran Date: Wed, 22 Oct 2014 03:55:17 -0300 Subject: [PATCH 1/8] Fix support for custom root in JSON-API adapter --- lib/active_model/serializer/adapter/json_api.rb | 2 +- test/action_controller/serialization_test.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index 71327b3c..0d93cac5 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -8,7 +8,7 @@ module ActiveModel end def serializable_hash(options = {}) - @root = (options[:root] || serializer.json_key).to_s.pluralize.to_sym + @root = (options[:root] || serializer.json_key.to_s.pluralize).to_sym @hash = {} if serializer.respond_to?(:each) diff --git a/test/action_controller/serialization_test.rb b/test/action_controller/serialization_test.rb index 4103e273..fbbb48f9 100644 --- a/test/action_controller/serialization_test.rb +++ b/test/action_controller/serialization_test.rb @@ -24,6 +24,16 @@ module ActionController ActiveModel::Serializer.config.adapter = old_adapter end + def render_using_custom_root_in_adapter_with_a_default + old_adapter = ActiveModel::Serializer.config.adapter + # JSON-API adapter sets root by default + ActiveModel::Serializer.config.adapter = ActiveModel::Serializer::Adapter::JsonApi + @profile = Profile.new({ name: 'Name 1', description: 'Description 1', comments: 'Comments 1' }) + render json: @profile, root: "profile" + ensure + ActiveModel::Serializer.config.adapter = old_adapter + end + def render_array_using_implicit_serializer array = [ Profile.new({ name: 'Name 1', description: 'Description 1', comments: 'Comments 1' }), @@ -57,6 +67,13 @@ module ActionController assert_equal '{"profiles":{"name":"Name 1","description":"Description 1"}}', @response.body end + def test_render_using_custom_root_in_adapter_with_a_default + get :render_using_custom_root_in_adapter_with_a_default + + assert_equal 'application/json', @response.content_type + assert_equal '{"profile":{"name":"Name 1","description":"Description 1"}}', @response.body + end + def test_render_array_using_implicit_serializer get :render_array_using_implicit_serializer assert_equal 'application/json', @response.content_type From f9b7c742359092e12447906af3d3800c43d0102e Mon Sep 17 00:00:00 2001 From: Edgars Jekabsons Date: Wed, 22 Oct 2014 15:30:44 +0300 Subject: [PATCH 2/8] Renamed embed test to have "test" suffix Fixed reference to association options --- lib/active_model/serializer/adapter/json_api.rb | 2 +- .../{has_many_embed_ids.rb => has_many_embed_ids_test.rb} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename test/adapter/json_api/{has_many_embed_ids.rb => has_many_embed_ids_test.rb} (100%) diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index 71327b3c..34ca48a5 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -18,7 +18,7 @@ module ActiveModel serializer.each_association do |name, association, opts| @hash[@root][:links] ||= {} - unless options[:embed] == :ids + unless opts[:embed] == :ids @hash[:linked] ||= {} end diff --git a/test/adapter/json_api/has_many_embed_ids.rb b/test/adapter/json_api/has_many_embed_ids_test.rb similarity index 100% rename from test/adapter/json_api/has_many_embed_ids.rb rename to test/adapter/json_api/has_many_embed_ids_test.rb From 19ac1398802ada24a9938dadc5cd1a36e4944308 Mon Sep 17 00:00:00 2001 From: Guillermo Iguaran Date: Thu, 30 Oct 2014 09:35:05 -0500 Subject: [PATCH 3/8] Handle correctly null associations null belongs_to associations are now serialized as nil instead of raise an error during serialization. --- lib/active_model/serializer.rb | 2 +- lib/active_model/serializer/adapter/json.rb | 6 +++++- lib/active_model/serializer/adapter/json_api.rb | 14 +++++++++----- test/adapter/json/belongs_to_test.rb | 11 +++++++++++ test/adapter/json/collection_test.rb | 7 +++++-- test/adapter/json/has_many_test.rb | 2 ++ test/adapter/json_api/belongs_to_test.rb | 12 ++++++++++++ test/adapter/json_api/collection_test.rb | 7 +++++-- test/adapter/json_api/has_many_test.rb | 2 ++ test/adapter/json_test.rb | 2 ++ test/fixtures/poro.rb | 1 + test/serializers/associations_test.rb | 15 +++++++++------ 12 files changed, 64 insertions(+), 17 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 7050dc38..dbc2060d 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -133,7 +133,7 @@ module ActiveModel self.class._associations.dup.each do |name, options| association = object.send(name) serializer_class = ActiveModel::Serializer.serializer_for(association) - serializer = serializer_class.new(association) + serializer = serializer_class.new(association) if serializer_class if block_given? block.call(name, serializer, options[:options]) diff --git a/lib/active_model/serializer/adapter/json.rb b/lib/active_model/serializer/adapter/json.rb index 3b568194..8ad1e41d 100644 --- a/lib/active_model/serializer/adapter/json.rb +++ b/lib/active_model/serializer/adapter/json.rb @@ -13,7 +13,11 @@ module ActiveModel array_serializer = association @result[name] = array_serializer.map { |item| item.attributes(opts) } else - @result[name] = association.attributes(options) + if association + @result[name] = association.attributes(options) + else + @result[name] = nil + end end end end diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index 3f0057a8..f5ff7128 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -44,13 +44,17 @@ module ActiveModel end def add_link(name, serializer, options) - @hash[@root][:links][name] = serializer.id.to_s + if serializer + @hash[@root][:links][name] = serializer.id.to_s - unless options[:embed] == :ids - plural_name = name.to_s.pluralize.to_sym + unless options[:embed] == :ids + plural_name = name.to_s.pluralize.to_sym - @hash[:linked][plural_name] ||= [] - @hash[:linked][plural_name].push attributes_for_serializer(serializer, options) + @hash[:linked][plural_name] ||= [] + @hash[:linked][plural_name].push attributes_for_serializer(serializer, options) + end + else + @hash[@root][:links][name] = nil end end diff --git a/test/adapter/json/belongs_to_test.rb b/test/adapter/json/belongs_to_test.rb index 498f1d98..663907de 100644 --- a/test/adapter/json/belongs_to_test.rb +++ b/test/adapter/json/belongs_to_test.rb @@ -7,9 +7,13 @@ module ActiveModel class BelongsToTest < Minitest::Test def setup @post = Post.new(id: 42, title: 'New Post', body: 'Body') + @anonymous_post = Post.new(id: 43, title: 'Hello!!', body: 'Hello, world!!') @comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') @post.comments = [@comment] + @anonymous_post.comments = [] @comment.post = @post + @post.author = @author + @anonymous_post.author = nil @serializer = CommentSerializer.new(@comment) @adapter = ActiveModel::Serializer::Adapter::Json.new(@serializer) @@ -18,6 +22,13 @@ module ActiveModel def test_includes_post assert_equal({id: 42, title: 'New Post', body: 'Body'}, @adapter.serializable_hash[:post]) end + + def test_include_nil_author + serializer = PostSerializer.new(@anonymous_post) + adapter = ActiveModel::Serializer::Adapter::Json.new(serializer) + + assert_equal({title: "Hello!!", body: "Hello, world!!", id: 43, comments: [], author: nil}, adapter.serializable_hash) + end end end end diff --git a/test/adapter/json/collection_test.rb b/test/adapter/json/collection_test.rb index 4d02f65a..51af5448 100644 --- a/test/adapter/json/collection_test.rb +++ b/test/adapter/json/collection_test.rb @@ -6,10 +6,13 @@ module ActiveModel class Json class Collection < Minitest::Test def setup + @author = Author.new(id: 1, name: 'Steve K.') @first_post = Post.new(id: 1, title: 'Hello!!', body: 'Hello, world!!') @second_post = Post.new(id: 2, title: 'New Post', body: 'Body') @first_post.comments = [] @second_post.comments = [] + @first_post.author = @author + @second_post.author = @author @serializer = ArraySerializer.new([@first_post, @second_post]) @adapter = ActiveModel::Serializer::Adapter::Json.new(@serializer) @@ -17,8 +20,8 @@ module ActiveModel def test_include_multiple_posts assert_equal([ - {title: "Hello!!", body: "Hello, world!!", id: 1, comments: []}, - {title: "New Post", body: "Body", id: 2, comments: []} + {title: "Hello!!", body: "Hello, world!!", id: 1, comments: [], author: {id: 1, name: "Steve K."}}, + {title: "New Post", body: "Body", id: 2, comments: [], author: {id: 1, name: "Steve K."}} ], @adapter.serializable_hash) end end diff --git a/test/adapter/json/has_many_test.rb b/test/adapter/json/has_many_test.rb index d1534e6b..77f672c7 100644 --- a/test/adapter/json/has_many_test.rb +++ b/test/adapter/json/has_many_test.rb @@ -6,10 +6,12 @@ module ActiveModel class Json class HasManyTestTest < Minitest::Test def setup + @author = Author.new(id: 1, name: 'Steve K.') @post = Post.new(title: 'New Post', body: 'Body') @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT') @post.comments = [@first_comment, @second_comment] + @post.author = @author @first_comment.post = @post @second_comment.post = @post diff --git a/test/adapter/json_api/belongs_to_test.rb b/test/adapter/json_api/belongs_to_test.rb index 0a43e531..58a0f699 100644 --- a/test/adapter/json_api/belongs_to_test.rb +++ b/test/adapter/json_api/belongs_to_test.rb @@ -6,10 +6,15 @@ module ActiveModel class JsonApi class BelongsToTest < Minitest::Test def setup + @author = Author.new(id: 1, name: 'Steve K.') @post = Post.new(id: 42, title: 'New Post', body: 'Body') + @anonymous_post = Post.new(id: 43, title: 'Hello!!', body: 'Hello, world!!') @comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') @post.comments = [@comment] + @anonymous_post.comments = [] @comment.post = @post + @post.author = @author + @anonymous_post.author = nil @serializer = CommentSerializer.new(@comment) @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer) @@ -22,6 +27,13 @@ module ActiveModel def test_includes_linked_post assert_equal([{id: "42", title: 'New Post', body: 'Body'}], @adapter.serializable_hash[:linked][:posts]) end + + def test_include_nil_author + serializer = PostSerializer.new(@anonymous_post) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) + + assert_equal({comments: [], author: nil}, adapter.serializable_hash[:posts][:links]) + end end end end diff --git a/test/adapter/json_api/collection_test.rb b/test/adapter/json_api/collection_test.rb index 566581a8..5eaec7a5 100644 --- a/test/adapter/json_api/collection_test.rb +++ b/test/adapter/json_api/collection_test.rb @@ -6,10 +6,13 @@ module ActiveModel class JsonApi class Collection < Minitest::Test def setup + @author = Author.new(id: 1, name: 'Steve K.') @first_post = Post.new(id: 1, title: 'Hello!!', body: 'Hello, world!!') @second_post = Post.new(id: 2, title: 'New Post', body: 'Body') @first_post.comments = [] @second_post.comments = [] + @first_post.author = @author + @second_post.author = @author @serializer = ArraySerializer.new([@first_post, @second_post]) @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer) @@ -17,8 +20,8 @@ module ActiveModel def test_include_multiple_posts assert_equal([ - {title: "Hello!!", body: "Hello, world!!", id: "1", links: {comments: []}}, - {title: "New Post", body: "Body", id: "2", links: {comments: []}} + {title: "Hello!!", body: "Hello, world!!", id: "1", links: {comments: [], author: "1"}}, + {title: "New Post", body: "Body", id: "2", links: {comments: [], author: "1"}} ], @adapter.serializable_hash[:posts]) end end diff --git a/test/adapter/json_api/has_many_test.rb b/test/adapter/json_api/has_many_test.rb index f558c7d4..17c9c17d 100644 --- a/test/adapter/json_api/has_many_test.rb +++ b/test/adapter/json_api/has_many_test.rb @@ -6,12 +6,14 @@ module ActiveModel class JsonApi class HasManyTest < Minitest::Test def setup + @author = Author.new(id: 1, name: 'Steve K.') @post = Post.new(title: 'New Post', body: 'Body') @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT') @post.comments = [@first_comment, @second_comment] @first_comment.post = @post @second_comment.post = @post + @post.author = @author @serializer = PostSerializer.new(@post) @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer) diff --git a/test/adapter/json_test.rb b/test/adapter/json_test.rb index 1d2e35a6..fe573f7b 100644 --- a/test/adapter/json_test.rb +++ b/test/adapter/json_test.rb @@ -5,12 +5,14 @@ module ActiveModel class Adapter class JsonTest < Minitest::Test def setup + @author = Author.new(id: 1, name: 'Steve K.') @post = Post.new(title: 'New Post', body: 'Body') @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT') @post.comments = [@first_comment, @second_comment] @first_comment.post = @post @second_comment.post = @post + @post.author = @author @serializer = PostSerializer.new(@post) @adapter = ActiveModel::Serializer::Adapter::Json.new(@serializer) diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb index 2f4ffdbf..ce5f345a 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -43,6 +43,7 @@ PostSerializer = Class.new(ActiveModel::Serializer) do attributes :title, :body, :id has_many :comments + belongs_to :author url :comments end diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index 6d69b563..0891f365 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -2,7 +2,7 @@ require 'test_helper' module ActiveModel class Serializer - class AssocationsTest < Minitest::Test + class AssociationsTest < Minitest::Test class Model def initialize(hash={}) @attributes = hash @@ -25,20 +25,23 @@ module ActiveModel def setup + @author = Author.new(name: 'Steve K.') @post = Post.new({ title: 'New Post', body: 'Body' }) @comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' }) @post.comments = [@comment] @comment.post = @post + @post.author = @author + @author.posts = [@post] - @post_serializer = PostSerializer.new(@post) + @author_serializer = AuthorSerializer.new(@author) @comment_serializer = CommentSerializer.new(@comment) end def test_has_many - assert_equal({comments: {type: :has_many, options: {}}}, @post_serializer.class._associations) - @post_serializer.each_association do |name, serializer, options| - assert_equal(:comments, name) - assert_equal({}, options) + assert_equal({posts: {type: :has_many, options: {embed: :ids}}}, @author_serializer.class._associations) + @author_serializer.each_association do |name, serializer, options| + assert_equal(:posts, name) + assert_equal({embed: :ids}, options) assert_kind_of(ActiveModel::Serializer.config.array_serializer, serializer) end end From 2d21a8e83fa537ad3b5ea0fd9e961c781ff85cbd Mon Sep 17 00:00:00 2001 From: Guillermo Iguaran Date: Fri, 31 Oct 2014 01:54:13 -0500 Subject: [PATCH 4/8] Add type when association name is different than objects type --- .../serializer/adapter/json_api.rb | 23 ++++++++++++++++--- test/adapter/json_api/belongs_to_test.rb | 9 ++++++++ test/adapter/json_api/has_many_test.rb | 11 ++++++++- test/fixtures/poro.rb | 8 +++++++ 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index f5ff7128..de0f3f83 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -34,8 +34,18 @@ module ActiveModel end def add_links(name, serializers, options) - @hash[@root][:links][name] ||= [] - @hash[@root][:links][name] += serializers.map{|serializer| serializer.id.to_s } + if serializers.first + type = serializers.first.object.class.to_s.underscore.pluralize + end + if name.to_s == type || !type + @hash[@root][:links][name] ||= [] + @hash[@root][:links][name] += serializers.map{|serializer| serializer.id.to_s } + else + @hash[@root][:links][name] ||= {} + @hash[@root][:links][name][:type] = type + @hash[@root][:links][name][:ids] ||= [] + @hash[@root][:links][name][:ids] += serializers.map{|serializer| serializer.id.to_s } + end unless options[:embed] == :ids @hash[:linked][name] ||= [] @@ -45,7 +55,14 @@ module ActiveModel def add_link(name, serializer, options) if serializer - @hash[@root][:links][name] = serializer.id.to_s + type = serializer.object.class.to_s.underscore + if name.to_s == type || !type + @hash[@root][:links][name] = serializer.id.to_s + else + @hash[@root][:links][name] ||= {} + @hash[@root][:links][name][:type] = type + @hash[@root][:links][name][:id] = serializer.id.to_s + end unless options[:embed] == :ids plural_name = name.to_s.pluralize.to_sym diff --git a/test/adapter/json_api/belongs_to_test.rb b/test/adapter/json_api/belongs_to_test.rb index 58a0f699..0db303a6 100644 --- a/test/adapter/json_api/belongs_to_test.rb +++ b/test/adapter/json_api/belongs_to_test.rb @@ -15,6 +15,9 @@ module ActiveModel @comment.post = @post @post.author = @author @anonymous_post.author = nil + @blog = Blog.new(id: 1, name: "My Blog!!") + @blog.writer = @author + @blog.articles = [@post, @anonymous_post] @serializer = CommentSerializer.new(@comment) @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer) @@ -34,6 +37,12 @@ module ActiveModel assert_equal({comments: [], author: nil}, adapter.serializable_hash[:posts][:links]) end + + def test_include_type_for_association_when_is_different_than_name + serializer = BlogSerializer.new(@blog) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) + assert_equal({type: "author", id: "1"}, adapter.serializable_hash[:blogs][:links][:writer]) + end end end end diff --git a/test/adapter/json_api/has_many_test.rb b/test/adapter/json_api/has_many_test.rb index 17c9c17d..2f74195f 100644 --- a/test/adapter/json_api/has_many_test.rb +++ b/test/adapter/json_api/has_many_test.rb @@ -7,13 +7,16 @@ module ActiveModel class HasManyTest < Minitest::Test def setup @author = Author.new(id: 1, name: 'Steve K.') - @post = Post.new(title: 'New Post', body: 'Body') + @post = Post.new(id: 1, title: 'New Post', body: 'Body') @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT') @post.comments = [@first_comment, @second_comment] @first_comment.post = @post @second_comment.post = @post @post.author = @author + @blog = Blog.new(id: 1, name: "My Blog!!") + @blog.writer = @author + @blog.articles = [@post] @serializer = PostSerializer.new(@post) @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer) @@ -29,6 +32,12 @@ module ActiveModel {id: "2", body: 'ZOMG ANOTHER COMMENT'} ], @adapter.serializable_hash[:linked][:comments]) end + + def test_include_type_for_association_when_is_different_than_name + serializer = BlogSerializer.new(@blog) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) + assert_equal({type: "posts", ids: ["1"]}, adapter.serializable_hash[:blogs][:links][:articles]) + end end end end diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb index ce5f345a..77e3506f 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -38,6 +38,7 @@ end Post = Class.new(Model) Comment = Class.new(Model) Author = Class.new(Model) +Blog = Class.new(Model) PostSerializer = Class.new(ActiveModel::Serializer) do attributes :title, :body, :id @@ -58,3 +59,10 @@ AuthorSerializer = Class.new(ActiveModel::Serializer) do has_many :posts, embed: :ids end + +BlogSerializer = Class.new(ActiveModel::Serializer) do + attributes :id, :name + + belongs_to :writer + has_many :articles +end From 3bba334cf875cd3d9f53c4d1f9673558f71139d6 Mon Sep 17 00:00:00 2001 From: Guillermo Iguaran Date: Fri, 31 Oct 2014 14:30:59 -0500 Subject: [PATCH 5/8] JSON-API: Don't include linked section if associations are empty --- lib/active_model/serializer/adapter/json_api.rb | 8 +++----- test/adapter/json_api/has_many_test.rb | 10 ++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index de0f3f83..596d54c0 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -18,9 +18,6 @@ module ActiveModel serializer.each_association do |name, association, opts| @hash[@root][:links] ||= {} - unless opts[:embed] == :ids - @hash[:linked] ||= {} - end if association.respond_to?(:each) add_links(name, association, opts) @@ -47,7 +44,8 @@ module ActiveModel @hash[@root][:links][name][:ids] += serializers.map{|serializer| serializer.id.to_s } end - unless options[:embed] == :ids + unless options[:embed] == :ids || serializers.count == 0 + @hash[:linked] ||= {} @hash[:linked][name] ||= [] @hash[:linked][name] += serializers.map { |item| attributes_for_serializer(item, options) } end @@ -66,7 +64,7 @@ module ActiveModel unless options[:embed] == :ids plural_name = name.to_s.pluralize.to_sym - + @hash[:linked] ||= {} @hash[:linked][plural_name] ||= [] @hash[:linked][plural_name].push attributes_for_serializer(serializer, options) end diff --git a/test/adapter/json_api/has_many_test.rb b/test/adapter/json_api/has_many_test.rb index 2f74195f..6bcd43ca 100644 --- a/test/adapter/json_api/has_many_test.rb +++ b/test/adapter/json_api/has_many_test.rb @@ -8,12 +8,15 @@ module ActiveModel def setup @author = Author.new(id: 1, name: 'Steve K.') @post = Post.new(id: 1, title: 'New Post', body: 'Body') + @post_without_comments = Post.new(id: 2, title: 'Second Post', body: 'Second') @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT') @post.comments = [@first_comment, @second_comment] + @post_without_comments.comments = [] @first_comment.post = @post @second_comment.post = @post @post.author = @author + @post_without_comments.author = nil @blog = Blog.new(id: 1, name: "My Blog!!") @blog.writer = @author @blog.articles = [@post] @@ -33,6 +36,13 @@ module ActiveModel ], @adapter.serializable_hash[:linked][:comments]) end + def test_no_include_linked_if_comments_is_empty + serializer = PostSerializer.new(@post_without_comments) + adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) + + assert_nil adapter.serializable_hash[:linked] + end + def test_include_type_for_association_when_is_different_than_name serializer = BlogSerializer.new(@blog) adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer) From 3504c1a5d88296b69a241c7123bd7a45e14d600d Mon Sep 17 00:00:00 2001 From: Alexandre de Oliveira Date: Sun, 2 Nov 2014 01:23:29 -0200 Subject: [PATCH 6/8] Fixes rbx gems bundling on TravisCI --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index d54edc2c..ad22ffe3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,8 @@ rvm: - jruby-19mode - rbx-2 - ruby-head +install: + - bundle install --retry=3 env: - "RAILS_VERSION=3.2" - "RAILS_VERSION=4.0" From d5bae0c2f0a1e5709b42bca4015617938eb72624 Mon Sep 17 00:00:00 2001 From: Gary Gordon Date: Thu, 23 Oct 2014 11:46:51 -0400 Subject: [PATCH 7/8] Include 'linked' member for json-api collections The options passed to the render are partitioned into adapter options and serializer options. 'include' and 'root' are sent to the adapter, not sure what options would go directly to serializer, but leaving this in until I understand that better. --- .gitignore | 2 +- README.md | 67 ++++++----- lib/action_controller/serialization.rb | 10 +- lib/active_model/serializer/adapter.rb | 1 + .../serializer/adapter/json_api.rb | 47 +++++--- .../action_controller/json_api_linked_test.rb | 104 ++++++++++++++++++ test/adapter/json/belongs_to_test.rb | 3 +- test/adapter/json_api/belongs_to_test.rb | 3 + test/adapter/json_api/collection_test.rb | 7 +- .../json_api/has_many_embed_ids_test.rb | 2 + test/adapter/json_api/has_many_test.rb | 4 + test/adapter/json_api/linked_test.rb | 40 +++++++ test/fixtures/poro.rb | 1 + test/serializers/associations_test.rb | 15 ++- 14 files changed, 250 insertions(+), 56 deletions(-) create mode 100644 test/action_controller/json_api_linked_test.rb create mode 100644 test/adapter/json_api/linked_test.rb diff --git a/.gitignore b/.gitignore index 838a74e0..1ecf6e4d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ test/tmp test/version_tmp tmp *.swp -.ruby-version \ No newline at end of file +.ruby-version diff --git a/README.md b/README.md index 77f1a108..4121dfe2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# ActiveModel::Serializers - +# ActiveModel::Serializers + [![Build Status](https://travis-ci.org/rails-api/active_model_serializers.svg)](https://travis-ci.org/rails-api/active_model_serializers) -ActiveModel::Serializers brings convention over configuration to your JSON generation. +ActiveModel::Serializers brings convention over configuration to your JSON generation. AMS does this through two components: **serializers** and **adapters**. Serializers describe which attributes and relationships should be serialized. Adapters describe how attributes and relationships should be serialized. @@ -32,7 +32,7 @@ serializers: ```ruby class PostSerializer < ActiveModel::Serializer attributes :title, :body - + has_many :comments url :post @@ -61,7 +61,7 @@ ActiveModel::Serializer.config.adapter = ActiveModel::Serializer::Adapter::HalAd ``` or - + ```ruby ActiveModel::Serializer.config.adapter = :hal ``` @@ -85,18 +85,27 @@ end In this case, Rails will look for a serializer named `PostSerializer`, and if it exists, use it to serialize the `Post`. -## Installation - -Add this line to your application's Gemfile: +### Built in Adapters -``` -gem 'active_model_serializers' +The `:json_api` adapter will include the associated resources in the `"linked"` +member when the resource names are included in the `include` option. + +```ruby + render @posts, include: 'authors,comments' ``` - -And then execute: -``` -$ bundle +## Installation + +Add this line to your application's Gemfile: + +``` +gem 'active_model_serializers' +``` + +And then execute: + +``` +$ bundle ``` ## Creating a Serializer @@ -141,29 +150,27 @@ class CommentSerializer < ActiveModel::Serializer end ``` -The attribute names are a **whitelist** of attributes to be serialized. - +The attribute names are a **whitelist** of attributes to be serialized. + The `has_many` and `belongs_to` declarations describe relationships between -resources. By default, when you serialize a `Post`, you will -get its `Comment`s as well. +resources. By default, when you serialize a `Post`, you will get its `Comment`s +as well. The `url` declaration describes which named routes to use while generating URLs for your JSON. Not every adapter will require URLs. ## Getting Help -If you find a bug, please report an -[Issue](https://github.com/rails-api/active_model_serializers/issues/new). +If you find a bug, please report an [Issue](https://github.com/rails-api/active_model_serializers/issues/new). -If you have a question, please [post to Stack -Overflow](http://stackoverflow.com/questions/tagged/active-model-serializers). +If you have a question, please [post to Stack Overflow](http://stackoverflow.com/questions/tagged/active-model-serializers). Thanks! - -## Contributing - -1. Fork it ( https://github.com/rails-api/active_model_serializers/fork ) -2. Create your feature branch (`git checkout -b my-new-feature`) -3. Commit your changes (`git commit -am 'Add some feature'`) -4. Push to the branch (`git push origin my-new-feature`) -5. Create a new Pull Request + +## Contributing + +1. Fork it ( https://github.com/rails-api/active_model_serializers/fork ) +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create a new Pull Request diff --git a/lib/action_controller/serialization.rb b/lib/action_controller/serialization.rb index f26f0112..2b460cb4 100644 --- a/lib/action_controller/serialization.rb +++ b/lib/action_controller/serialization.rb @@ -6,15 +6,18 @@ module ActionController include ActionController::Renderers + ADAPTER_OPTION_KEYS = [:include, :root] + [:_render_option_json, :_render_with_renderer_json].each do |renderer_method| define_method renderer_method do |resource, options| serializer = ActiveModel::Serializer.serializer_for(resource) if serializer + adapter_opts, serializer_opts = + options.partition { |k, _| ADAPTER_OPTION_KEYS.include? k } # omg hax - object = serializer.new(resource, options) - adapter = ActiveModel::Serializer.adapter.new(object) - + object = serializer.new(resource, Hash[serializer_opts]) + adapter = ActiveModel::Serializer.adapter.new(object, Hash[adapter_opts]) super(adapter, options) else super(resource, options) @@ -23,4 +26,3 @@ module ActionController end end end - diff --git a/lib/active_model/serializer/adapter.rb b/lib/active_model/serializer/adapter.rb index f0a82573..864adbb4 100644 --- a/lib/active_model/serializer/adapter.rb +++ b/lib/active_model/serializer/adapter.rb @@ -10,6 +10,7 @@ module ActiveModel def initialize(serializer, options = {}) @serializer = serializer + @options = options end def serializable_hash(options = {}) diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index 596d54c0..693ec316 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -5,16 +5,19 @@ module ActiveModel def initialize(serializer, options = {}) super serializer.root = true + @hash = {} + @top = @options.fetch(:top) { @hash } end def serializable_hash(options = {}) - @root = (options[:root] || serializer.json_key.to_s.pluralize).to_sym - @hash = {} + @root = (@options[:root] || serializer.json_key.to_s.pluralize).to_sym if serializer.respond_to?(:each) - @hash[@root] = serializer.map{|s| self.class.new(s).serializable_hash[@root] } + @hash[@root] = serializer.map do |s| + self.class.new(s, @options.merge(top: @top)).serializable_hash[@root] + end else - @hash[@root] = attributes_for_serializer(serializer, {}) + @hash[@root] = attributes_for_serializer(serializer, @options) serializer.each_association do |name, association, opts| @hash[@root][:links] ||= {} @@ -44,10 +47,10 @@ module ActiveModel @hash[@root][:links][name][:ids] += serializers.map{|serializer| serializer.id.to_s } end - unless options[:embed] == :ids || serializers.count == 0 - @hash[:linked] ||= {} - @hash[:linked][name] ||= [] - @hash[:linked][name] += serializers.map { |item| attributes_for_serializer(item, options) } + unless serializers.none? || @options[:embed] == :ids + serializers.each do |serializer| + add_linked(name, serializer) + end end end @@ -62,17 +65,31 @@ module ActiveModel @hash[@root][:links][name][:id] = serializer.id.to_s end - unless options[:embed] == :ids - plural_name = name.to_s.pluralize.to_sym - @hash[:linked] ||= {} - @hash[:linked][plural_name] ||= [] - @hash[:linked][plural_name].push attributes_for_serializer(serializer, options) + unless @options[:embed] == :ids + add_linked(name, serializer) end else @hash[@root][:links][name] = nil end end + def add_linked(resource, serializer, parent = nil) + resource_path = [parent, resource].compact.join('.') + if include_assoc? resource_path + plural_name = resource.to_s.pluralize.to_sym + attrs = attributes_for_serializer(serializer, @options) + @top[:linked] ||= {} + @top[:linked][plural_name] ||= [] + @top[:linked][plural_name].push attrs unless @top[:linked][plural_name].include? attrs + end + + unless serializer.respond_to?(:each) + serializer.each_association do |name, association, opts| + add_linked(name, association, resource) if association + end + end + end + private def attributes_for_serializer(serializer, options) @@ -80,6 +97,10 @@ module ActiveModel attributes[:id] = attributes[:id].to_s if attributes[:id] attributes end + + def include_assoc? assoc + @options[:include] && @options[:include].split(',').include?(assoc.to_s) + end end end end diff --git a/test/action_controller/json_api_linked_test.rb b/test/action_controller/json_api_linked_test.rb new file mode 100644 index 00000000..f02e90e2 --- /dev/null +++ b/test/action_controller/json_api_linked_test.rb @@ -0,0 +1,104 @@ +require 'test_helper' + +module ActionController + module Serialization + class JsonApiLinkedTest < ActionController::TestCase + class MyController < ActionController::Base + def setup_post + @author = Author.new(id: 1, name: 'Steve K.') + @author.posts = [] + @author2 = Author.new(id: 2, name: 'Anonymous') + @author2.posts = [] + @post = Post.new(id: 1, title: 'New Post', body: 'Body') + @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') + @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT') + @post.comments = [@first_comment, @second_comment] + @post.author = @author + @first_comment.post = @post + @first_comment.author = @author2 + @second_comment.post = @post + @second_comment.author = nil + end + + def with_json_api_adapter + old_adapter = ActiveModel::Serializer.config.adapter + ActiveModel::Serializer.config.adapter = :json_api + yield + ensure + ActiveModel::Serializer.config.adapter = old_adapter + end + + def render_resource_without_include + with_json_api_adapter do + setup_post + render json: @post + end + end + + def render_resource_with_include + with_json_api_adapter do + setup_post + render json: @post, include: 'author' + end + end + + def render_resource_with_nested_include + with_json_api_adapter do + setup_post + render json: @post, include: 'comments.author' + end + end + + def render_collection_without_include + with_json_api_adapter do + setup_post + render json: [@post] + end + end + + def render_collection_with_include + with_json_api_adapter do + setup_post + render json: [@post], include: 'author,comments' + end + end + end + + tests MyController + + def test_render_resource_without_include + get :render_resource_without_include + response = JSON.parse(@response.body) + refute response.key? 'linked' + end + + def test_render_resource_with_include + get :render_resource_with_include + response = JSON.parse(@response.body) + assert response.key? 'linked' + assert_equal 1, response['linked']['authors'].size + assert_equal 'Steve K.', response['linked']['authors'].first['name'] + end + + def test_render_resource_with_nested_include + get :render_resource_with_nested_include + response = JSON.parse(@response.body) + assert response.key? 'linked' + assert_equal 1, response['linked']['authors'].size + assert_equal 'Anonymous', response['linked']['authors'].first['name'] + end + + def test_render_collection_without_include + get :render_collection_without_include + response = JSON.parse(@response.body) + refute response.key? 'linked' + end + + def test_render_collection_with_include + get :render_collection_with_include + response = JSON.parse(@response.body) + assert response.key? 'linked' + end + end + end +end diff --git a/test/adapter/json/belongs_to_test.rb b/test/adapter/json/belongs_to_test.rb index 663907de..ea118647 100644 --- a/test/adapter/json/belongs_to_test.rb +++ b/test/adapter/json/belongs_to_test.rb @@ -11,8 +11,9 @@ module ActiveModel @comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') @post.comments = [@comment] @anonymous_post.comments = [] - @comment.post = @post @post.author = @author + @comment.post = @post + @comment.author = nil @anonymous_post.author = nil @serializer = CommentSerializer.new(@comment) diff --git a/test/adapter/json_api/belongs_to_test.rb b/test/adapter/json_api/belongs_to_test.rb index 0db303a6..e25a71d8 100644 --- a/test/adapter/json_api/belongs_to_test.rb +++ b/test/adapter/json_api/belongs_to_test.rb @@ -13,11 +13,13 @@ module ActiveModel @post.comments = [@comment] @anonymous_post.comments = [] @comment.post = @post + @comment.author = nil @post.author = @author @anonymous_post.author = nil @blog = Blog.new(id: 1, name: "My Blog!!") @blog.writer = @author @blog.articles = [@post, @anonymous_post] + @author.posts = [] @serializer = CommentSerializer.new(@comment) @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer) @@ -28,6 +30,7 @@ module ActiveModel end def test_includes_linked_post + @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'post') assert_equal([{id: "42", title: 'New Post', body: 'Body'}], @adapter.serializable_hash[:linked][:posts]) end diff --git a/test/adapter/json_api/collection_test.rb b/test/adapter/json_api/collection_test.rb index 5eaec7a5..922103ea 100644 --- a/test/adapter/json_api/collection_test.rb +++ b/test/adapter/json_api/collection_test.rb @@ -4,7 +4,7 @@ module ActiveModel class Serializer class Adapter class JsonApi - class Collection < Minitest::Test + class CollectionTest < Minitest::Test def setup @author = Author.new(id: 1, name: 'Steve K.') @first_post = Post.new(id: 1, title: 'Hello!!', body: 'Hello, world!!') @@ -13,6 +13,7 @@ module ActiveModel @second_post.comments = [] @first_post.author = @author @second_post.author = @author + @author.posts = [@first_post, @second_post] @serializer = ArraySerializer.new([@first_post, @second_post]) @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer) @@ -20,8 +21,8 @@ module ActiveModel def test_include_multiple_posts assert_equal([ - {title: "Hello!!", body: "Hello, world!!", id: "1", links: {comments: [], author: "1"}}, - {title: "New Post", body: "Body", id: "2", links: {comments: [], author: "1"}} + { title: "Hello!!", body: "Hello, world!!", id: "1", links: { comments: [], author: "1" } }, + { title: "New Post", body: "Body", id: "2", links: { comments: [], author: "1" } } ], @adapter.serializable_hash[:posts]) end end diff --git a/test/adapter/json_api/has_many_embed_ids_test.rb b/test/adapter/json_api/has_many_embed_ids_test.rb index d5c448b5..4690e3c6 100644 --- a/test/adapter/json_api/has_many_embed_ids_test.rb +++ b/test/adapter/json_api/has_many_embed_ids_test.rb @@ -12,6 +12,8 @@ module ActiveModel @author.posts = [@first_post, @second_post] @first_post.author = @author @second_post.author = @author + @first_post.comments = [] + @second_post.comments = [] @serializer = AuthorSerializer.new(@author) @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer) diff --git a/test/adapter/json_api/has_many_test.rb b/test/adapter/json_api/has_many_test.rb index 6bcd43ca..975c5b63 100644 --- a/test/adapter/json_api/has_many_test.rb +++ b/test/adapter/json_api/has_many_test.rb @@ -7,10 +7,13 @@ module ActiveModel class HasManyTest < Minitest::Test def setup @author = Author.new(id: 1, name: 'Steve K.') + @author.posts = [] @post = Post.new(id: 1, title: 'New Post', body: 'Body') @post_without_comments = Post.new(id: 2, title: 'Second Post', body: 'Second') @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') + @first_comment.author = nil @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT') + @second_comment.author = nil @post.comments = [@first_comment, @second_comment] @post_without_comments.comments = [] @first_comment.post = @post @@ -30,6 +33,7 @@ module ActiveModel end def test_includes_linked_comments + @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'comments') assert_equal([ {id: "1", body: 'ZOMG A COMMENT'}, {id: "2", body: 'ZOMG ANOTHER COMMENT'} diff --git a/test/adapter/json_api/linked_test.rb b/test/adapter/json_api/linked_test.rb new file mode 100644 index 00000000..160d3daa --- /dev/null +++ b/test/adapter/json_api/linked_test.rb @@ -0,0 +1,40 @@ +require 'test_helper' + +module ActiveModel + class Serializer + class Adapter + class JsonApi + class LinkedTest < Minitest::Test + def setup + @author = Author.new(id: 1, name: 'Steve K.') + @first_post = Post.new(id: 1, title: 'Hello!!', body: 'Hello, world!!') + @second_post = Post.new(id: 2, title: 'New Post', body: 'Body') + @first_post.comments = [] + @second_post.comments = [] + @first_post.author = @author + @second_post.author = @author + @author.posts = [@first_post, @second_post] + + @serializer = ArraySerializer.new([@first_post, @second_post]) + @adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'author,comments') + end + + def test_include_multiple_posts_and_linked + @first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') + @second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT') + @first_post.comments = [@first_comment, @second_comment] + @first_comment.post = @first_post + @first_comment.author = nil + @second_comment.post = @first_post + @second_comment.author = nil + assert_equal([ + { title: "Hello!!", body: "Hello, world!!", id: "1", links: { comments: ['1', '2'], author: "1" } }, + { title: "New Post", body: "Body", id: "2", links: { comments: [], :author => "1" } } + ], @adapter.serializable_hash[:posts]) + assert_equal({ :comments => [{ :id => "1", :body => "ZOMG A COMMENT" }, { :id => "2", :body => "ZOMG ANOTHER COMMENT" }], :authors => [{ :id => "1", :name => "Steve K." }] }, @adapter.serializable_hash[:linked]) + end + end + end + end + end +end diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb index 77e3506f..7d8d57b4 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -52,6 +52,7 @@ CommentSerializer = Class.new(ActiveModel::Serializer) do attributes :id, :body belongs_to :post + belongs_to :author end AuthorSerializer = Class.new(ActiveModel::Serializer) do diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index 0891f365..99162acc 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -30,6 +30,7 @@ module ActiveModel @comment = Comment.new({ id: 1, body: 'ZOMG A COMMENT' }) @post.comments = [@comment] @comment.post = @post + @comment.author = nil @post.author = @author @author.posts = [@post] @@ -47,11 +48,17 @@ module ActiveModel end def test_has_one - assert_equal({post: {type: :belongs_to, options: {}}}, @comment_serializer.class._associations) + assert_equal({post: {type: :belongs_to, options: {}}, :author=>{:type=>:belongs_to, :options=>{}}}, @comment_serializer.class._associations) @comment_serializer.each_association do |name, serializer, options| - assert_equal(:post, name) - assert_equal({}, options) - assert_kind_of(PostSerializer, serializer) + if name == :post + assert_equal({}, options) + assert_kind_of(PostSerializer, serializer) + elsif name == :author + assert_equal({}, options) + assert_nil serializer + else + flunk "Unknown association: #{name}" + end end end end From f5411f045f87b4c45177d812714cdf7b6827f0b5 Mon Sep 17 00:00:00 2001 From: Guillermo Iguaran Date: Mon, 3 Nov 2014 17:38:58 -0500 Subject: [PATCH 8/8] Define as_json instead of to_json --- lib/active_model/serializer/adapter.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/active_model/serializer/adapter.rb b/lib/active_model/serializer/adapter.rb index f0a82573..3a883dad 100644 --- a/lib/active_model/serializer/adapter.rb +++ b/lib/active_model/serializer/adapter.rb @@ -16,8 +16,8 @@ module ActiveModel raise NotImplementedError, 'This is an abstract method. Should be implemented at the concrete adapter.' end - def to_json(options = {}) - serializable_hash(options).to_json + def as_json(options = {}) + serializable_hash(options) end end end