Skip to content

Commit

Permalink
Merge pull request #692 from ggordon/linked_for_jsonapi_collection
Browse files Browse the repository at this point in the history
Include 'linked' member for json-api collections
  • Loading branch information
kurko committed Nov 4, 2014
2 parents 8374396 + d5bae0c commit 95d1220
Show file tree
Hide file tree
Showing 14 changed files with 250 additions and 56 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ test/tmp
test/version_tmp
tmp
*.swp
.ruby-version
.ruby-version
67 changes: 37 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -32,7 +32,7 @@ serializers:
```ruby
class PostSerializer < ActiveModel::Serializer
attributes :title, :body

has_many :comments

url :post
Expand Down Expand Up @@ -61,7 +61,7 @@ ActiveModel::Serializer.config.adapter = ActiveModel::Serializer::Adapter::HalAd
```

or

```ruby
ActiveModel::Serializer.config.adapter = :hal
```
Expand All @@ -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

The `:json_api` adapter will include the associated resources in the `"linked"`
member when the resource names are included in the `include` option.

```
gem 'active_model_serializers'
```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
Expand Down Expand Up @@ -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
10 changes: 6 additions & 4 deletions lib/action_controller/serialization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ module Serialization

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)
Expand All @@ -23,4 +26,3 @@ module Serialization
end
end
end

1 change: 1 addition & 0 deletions lib/active_model/serializer/adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Adapter

def initialize(serializer, options = {})
@serializer = serializer
@options = options
end

def serializable_hash(options = {})
Expand Down
47 changes: 34 additions & 13 deletions lib/active_model/serializer/adapter/json_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ class JsonApi < Adapter
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] ||= {}
Expand Down Expand Up @@ -44,10 +47,10 @@ def add_links(name, serializers, options)
@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

Expand All @@ -62,24 +65,42 @@ def add_link(name, serializer, options)
@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)
attributes = serializer.attributes(options)
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
Expand Down
104 changes: 104 additions & 0 deletions test/action_controller/json_api_linked_test.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion test/adapter/json/belongs_to_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ def setup
@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)
Expand Down
3 changes: 3 additions & 0 deletions test/adapter/json_api/belongs_to_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ def setup
@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)
Expand All @@ -28,6 +30,7 @@ def test_includes_post_id
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

Expand Down
Loading

0 comments on commit 95d1220

Please sign in to comment.