Skip to content

Commit

Permalink
Include 'linked' member for json-api collections
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ggordon committed Nov 3, 2014
1 parent 80ece39 commit d5bae0c
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 d5bae0c

Please sign in to comment.