Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Respond with valid json when relationship is nil #1

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Misc:

Breaking changes:
- [#1662](https://github.com/rails-api/active_model_serializers/pull/1662) Drop support for Rails 4.0 and Ruby 2.0.0. (@remear)
- [#1720](https://github.com/rails-api/active_model_serializers/pull/1720) Block relationships must explicitly use `load_data` instead of the return value to customize relationship contents.

Features:
- [#1677](https://github.com/rails-api/active_model_serializers/pull/1677) Add `assert_schema`, `assert_request_schema`, `assert_request_response_schema`. (@bf4)
Expand All @@ -31,6 +32,7 @@ Features:
- [#1687](https://github.com/rails-api/active_model_serializers/pull/1687) Only calculate `_cache_digest` (in `cache_key`) when `skip_digest` is false. (@bf4)
- [#1647](https://github.com/rails-api/active_model_serializers/pull/1647) Restrict usage of `serializable_hash` options
to the ActiveModel::Serialization and ActiveModel::Serializers::JSON interface. (@bf4)
- [#1720](https://github.com/rails-api/active_model_serializers/pull/1720) Support JSONAPI 'include` parameter with links/loading.

Fixes:
- [#1700](https://github.com/rails-api/active_model_serializers/pull/1700) Support pagination link for Kaminari when no data is returned. (@iamnader)
Expand Down
37 changes: 37 additions & 0 deletions docs/general/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,43 @@ The user could pass in `include=**`.

We recommend filtering any user-supplied includes appropriately.

#### Include Parameters

You may wish to include related resources based on the `include` request
parameter (See [fetching_includes](http://jsonapi.org/format/#fetching-includes). You can opt-in to this pattern:

```ruby
class PostSerializer < ActiveModel::Serializer
associations_via_include_param(true)
end
```

The serializer will now behave as follows:

* If the relationship is in the `include` parameter, both
`relationships` and `included` will be populated.
* If the relationship is not in the `include` parameter, but a `link`
has been specified, `relationships` will contain the link but not
`id/type`s. This avoids an unnecessary database hit.
* If the relationship is not in the `include` parameter, and there is no
`link`, the relationship is rendered without `data`. This specifies
that the relationship exists, but we aren't saying anything about the
contents of that relationship (nil, empty, or otherwise).

To sync your URL `include` parameter with the `include` option passed to
`render`, you'll probably also want to add something like this:

```ruby
class PeopleController < ApplicationController
def index
render json: Person.all, include: params[:include]
end
end
```

Remember, you probably want to [whitelist your includes for security
purposes](https://github.com/rails-api/active_model_serializers/blob/master/docs/general/adapters.md#security-considerations).

## Advanced adapter configuration

### Registering an adapter
Expand Down
6 changes: 3 additions & 3 deletions docs/general/serializers.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ has_many :comments, key: :reviews
has_many :comments, serializer: CommentPreviewSerializer
has_many :reviews, virtual_value: [{ id: 1 }, { id: 2 }]
has_many :comments, key: :last_comments do
last(1)
load_data { last(1) }
end
```

Expand Down Expand Up @@ -252,7 +252,7 @@ class PostSerializer < ActiveModel::Serializer

# scope comments to those created_by the current user
has_many :comments do
object.comments.where(created_by: current_user)
load_data { object.comments.where(created_by: current_user) }
end
end
```
Expand Down Expand Up @@ -365,7 +365,7 @@ To override an association, call `has_many`, `has_one` or `belongs_to` with a bl
```ruby
class PostSerializer < ActiveModel::Serializer
has_many :comments do
object.comments.active
load_data { object.comments.active }
end
end
```
Expand Down
11 changes: 9 additions & 2 deletions lib/active_model/serializer/associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module Associations
included do
with_options instance_writer: false, instance_reader: true do |serializer|
serializer.class_attribute :_reflections
serializer.class_attribute :_associations_via_include_param
self._reflections ||= []
end

Expand Down Expand Up @@ -65,6 +66,10 @@ def has_one(name, options = {}, &block)
associate(HasOneReflection.new(name, options, block))
end

def associations_via_include_param(val)
self._associations_via_include_param = val
end

private

# Add reflection and define {name} accessor.
Expand All @@ -82,15 +87,17 @@ def associate(reflection)
# +default_include_directive+ config value when not provided)
# @return [Enumerator<Association>]
#
def associations(include_directive = ActiveModelSerializers.default_include_directive)
def associations(include_directive = ActiveModelSerializers.default_include_directive, current_include_directive = nil)
current_include_directive ||= include_directive
return unless object

Enumerator.new do |y|
self.class._reflections.each do |reflection|
next if reflection.excluded?(self)
key = reflection.options.fetch(:key, reflection.name)
next unless include_directive.key?(key)
y.yield reflection.build_association(self, instance_options)

y.yield reflection.build_association(self, instance_options, current_include_directive)
end
end
end
Expand Down
48 changes: 39 additions & 9 deletions lib/active_model/serializer/reflection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def initialize(*)
super
@_links = {}
@_include_data = true
@_load_data = false
@_meta = nil
end

Expand All @@ -56,6 +57,11 @@ def include_data(value = true)
:nil
end

def load_data(&blk)
@_load_data = blk
:nil
end

# @param serializer [ActiveModel::Serializer]
# @yield [ActiveModel::Serializer]
# @return [:nil, associated resource or resource collection]
Expand All @@ -69,19 +75,23 @@ def include_data(value = true)
# Blog.find(object.blog_id)
# end
# end
def value(serializer)
def value(serializer, current_include_directive)
@object = serializer.object
@scope = serializer.scope

if block
block_value = instance_exec(serializer, &block)
if block_value != :nil
block_value
elsif @_include_data
serializer.read_attribute_for_serialization(name)
load_data { block_value } if block_value != :nil

if include_data?(serializer, current_include_directive)
if @_load_data
@_load_data.call(current_include_directive)
else
include_data_for(serializer, current_include_directive)
end
end
else
serializer.read_attribute_for_serialization(name)
include_data_for(serializer, current_include_directive)
end
end

Expand All @@ -106,11 +116,11 @@ def value(serializer)
#
# @api private
#
def build_association(subject, parent_serializer_options)
association_value = value(subject)
def build_association(subject, parent_serializer_options, current_include_directive = {})
association_value = value(subject, current_include_directive)
reflection_options = options.dup
serializer_class = subject.class.serializer_for(association_value, reflection_options)
reflection_options[:include_data] = @_include_data
reflection_options[:include_data] = include_data?(subject, current_include_directive)

if serializer_class
begin
Expand All @@ -134,6 +144,26 @@ def build_association(subject, parent_serializer_options)

private

def include_data_for(serializer, current_include_directive)
return unless include_data?(serializer, current_include_directive)

if serializer.class._associations_via_include_param
if current_include_directive.key?(name)
serializer.read_attribute_for_serialization(name)
end
else
serializer.read_attribute_for_serialization(name)
end
end

def include_data?(serializer, current_include_directive)
if serializer.class._associations_via_include_param
current_include_directive.key?(name)
else
@_include_data
end
end

def serializer_options(subject, parent_serializer_options, reflection_options)
serializer = reflection_options.fetch(:serializer, nil)

Expand Down
18 changes: 9 additions & 9 deletions lib/active_model_serializers/adapter/json_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -235,17 +235,17 @@ def resource_objects_for(serializers)
@primary = []
@included = []
@resource_identifiers = Set.new
serializers.each { |serializer| process_resource(serializer, true) }
serializers.each { |serializer| process_resource(serializer, true, @include_directive) }
serializers.each { |serializer| process_relationships(serializer, @include_directive) }

[@primary, @included]
end

def process_resource(serializer, primary)
def process_resource(serializer, primary, include_directive = {})
resource_identifier = ResourceIdentifier.new(serializer, instance_options).as_json
return false unless @resource_identifiers.add?(resource_identifier)

resource_object = resource_object_for(serializer)
resource_object = resource_object_for(serializer, include_directive)
if primary
@primary << resource_object
else
Expand All @@ -267,7 +267,7 @@ def process_relationship(serializer, include_directive)
return
end
return unless serializer && serializer.object
return unless process_resource(serializer, false)
return unless process_resource(serializer, false, include_directive)

process_relationships(serializer, include_directive)
end
Expand All @@ -293,7 +293,7 @@ def attributes_for(serializer, fields)
end

# {http://jsonapi.org/format/#document-resource-objects Document Resource Objects}
def resource_object_for(serializer)
def resource_object_for(serializer, include_directive = {})
resource_object = serializer.fetch(self) do
resource_object = ResourceIdentifier.new(serializer, instance_options).as_json

Expand All @@ -304,7 +304,7 @@ def resource_object_for(serializer)
end

requested_associations = fieldset.fields_for(resource_object[:type]) || '*'
relationships = relationships_for(serializer, requested_associations)
relationships = relationships_for(serializer, requested_associations, include_directive)
resource_object[:relationships] = relationships if relationships.any?

links = links_for(serializer)
Expand Down Expand Up @@ -432,12 +432,12 @@ def resource_object_for(serializer)
# id: 'required-id',
# meta: meta
# }.reject! {|_,v| v.nil? }
def relationships_for(serializer, requested_associations)
include_directive = JSONAPI::IncludeDirective.new(
def relationships_for(serializer, requested_associations, current_include_directive)
full_include_directive = JSONAPI::IncludeDirective.new(
requested_associations,
allow_wildcard: true
)
serializer.associations(include_directive).each_with_object({}) do |association, hash|
serializer.associations(full_include_directive, current_include_directive).each_with_object({}) do |association, hash|
hash[association.key] = Relationship.new(
serializer,
association.serializer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def initialize(parent_serializer, serializer, serializable_resource_options, arg

def as_json
hash = {}
hash[:data] = data if association_options[:include_data]
hash[:data] = data || { meta: {} } if association_options[:include_data]
links = self.links
hash[:links] = links if links.any?
meta = self.meta
Expand Down
2 changes: 1 addition & 1 deletion test/action_controller/json_api/linked_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def test_render_resource_with_nested_has_many_include
'relationships' => {
'posts' => { 'data' => [] },
'roles' => { 'data' => [{ 'type' => 'roles', 'id' => '1' }, { 'type' => 'roles', 'id' => '2' }] },
'bio' => { 'data' => nil }
'bio' => { 'data' => { 'meta' => {} } }
}
}, {
'id' => '1',
Expand Down
6 changes: 3 additions & 3 deletions test/adapter/json_api/belongs_to_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def test_include_nil_author
serializer = PostSerializer.new(@anonymous_post)
adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer)

assert_equal({ comments: { data: [] }, blog: { data: { type: 'blogs', id: '999' } }, author: { data: nil } }, adapter.serializable_hash[:data][:relationships])
assert_equal({ comments: { data: [] }, blog: { data: { type: 'blogs', id: '999' } }, author: { data: { meta: {} } } }, adapter.serializable_hash[:data][:relationships])
end

def test_include_type_for_association_when_different_than_name
Expand Down Expand Up @@ -119,7 +119,7 @@ def test_include_linked_resources_with_type_name
relationships: {
posts: { data: [] },
roles: { data: [] },
bio: { data: nil }
bio: { data: { meta: {} } }
}
}, {
id: '42',
Expand All @@ -143,7 +143,7 @@ def test_include_linked_resources_with_type_name
relationships: {
comments: { data: [] },
blog: { data: { type: 'blogs', id: '999' } },
author: { data: nil }
author: { data: { meta: {} } }
}
}
]
Expand Down
2 changes: 1 addition & 1 deletion test/adapter/json_api/has_many_explicit_serializer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def test_includes_author_id
def test_explicit_serializer_with_null_resource
@post.author = nil

expected = { data: nil }
expected = { data: { meta: {} } }

assert_equal(expected, @adapter.serializable_hash[:data][:relationships][:author])
end
Expand Down
8 changes: 4 additions & 4 deletions test/adapter/json_api/has_many_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def test_includes_linked_comments
},
relationships: {
post: { data: { type: 'posts', id: '1' } },
author: { data: nil }
author: { data: { meta: {} } }
}
}, {
id: '2',
Expand All @@ -60,7 +60,7 @@ def test_includes_linked_comments
},
relationships: {
post: { data: { type: 'posts', id: '1' } },
author: { data: nil }
author: { data: { meta: {} } }
}
}]
assert_equal expected, @adapter.serializable_hash[:included]
Expand All @@ -73,14 +73,14 @@ def test_limit_fields_of_linked_comments
type: 'comments',
relationships: {
post: { data: { type: 'posts', id: '1' } },
author: { data: nil }
author: { data: { meta: {} } }
}
}, {
id: '2',
type: 'comments',
relationships: {
post: { data: { type: 'posts', id: '1' } },
author: { data: nil }
author: { data: { meta: {} } }
}
}]
assert_equal expected, @adapter.serializable_hash[:included]
Expand Down
Loading