Skip to content

Create NestedJson adapter. #1073

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

Closed
wants to merge 4 commits into from
Closed
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
11 changes: 5 additions & 6 deletions lib/active_model/serializer/adapter/flatten_json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@ module ActiveModel
class Serializer
class Adapter
class FlattenJson < Json
def serializable_hash(options = {})
super
@result
end

private

# no-op: FlattenJson adapter does not include meta data, because it does not support root.
def rooting?
false
end

def include_meta(json)
json
end
end
end
end
end

103 changes: 70 additions & 33 deletions lib/active_model/serializer/adapter/json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,86 @@ module ActiveModel
class Serializer
class Adapter
class Json < Adapter
def serializable_hash(options = nil)
cattr_accessor :default_limit_depth, :default_check_depth_strategy
self.default_limit_depth = 1
self.default_check_depth_strategy = :trim

def serializable_hash options = nil
options ||= {}
if serializer.respond_to?(:each)
@result = serializer.map { |s| FlattenJson.new(s).serializable_hash(options) }
else
@hash = {}
@current_depth = options[:_current_depth] || 0
@without_root = options[:_without_root]
@limit_depth = options[:limit_depth] || default_limit_depth
@check_depth_strategy = options[:check_depth_strategy] || default_check_depth_strategy

@core = cache_check(serializer) do
serializer.attributes(options)
end
@result =
serialize_collection(serializer, options.merge(_without_root: true)) ||
serialize_attributes(options).merge(serialize_associations)
rooting? ? { root => @result } : @result
end

def fragment_cache(cached_hash, non_cached_hash)
Json::FragmentCache.new().fragment_cache(cached_hash, non_cached_hash)
end

private

def rooting?
!@without_root && (@current_depth == 0)
end

def serialize_object serializer, options = {}
if serializer.try(:object)
self.class.new(serializer).serializable_hash(options)
end
end

def serialize_collection serializers, options = {}
if serializers.respond_to?(:each)
serializers.map { |s| serialize_object(s, options) }
end
end

def serialize_attributes options
cache_check(serializer) do
serializer.attributes(options)
end
end

def serialize_associations
hash = {}
next_depth = @current_depth + 1
cascading_options = {
limit_depth: @limit_depth,
check_depth_strategy: @check_depth_strategy,
_current_depth: next_depth
}
unless too_deep? next_depth
serializer.associations.each do |association|
serializer = association.serializer
opts = association.options

if serializer.respond_to?(:each)
array_serializer = serializer
@hash[association.key] = array_serializer.map do |item|
cache_check(item) do
item.attributes(opts)
end
end
else
@hash[association.key] =
if serializer && serializer.object
cache_check(serializer) do
serializer.attributes(options)
end
elsif opts[:virtual_value]
opts[:virtual_value]
end
end
opts = association.options.merge(cascading_options)
hash[association.key] =
serialize_collection(serializer, opts) ||
serialize_object(serializer, opts) ||
opts[:virtual_value]
end
@result = @core.merge @hash
end

{ root => @result }
hash
end

def fragment_cache(cached_hash, non_cached_hash)
Json::FragmentCache.new().fragment_cache(cached_hash, non_cached_hash)
def too_deep? depth
if depth > @limit_depth
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

case @check_depth_strategy
when :pass
false
when :fail
fail 'Too deep associations.'
when :trim
true
end
else
false
end
end

end
end
end
Expand Down
77 changes: 77 additions & 0 deletions test/adapter/json/nested_json_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
require 'test_helper'

module ActiveModel
class Serializer
class Adapter
class NestedJsonTest < Minitest::Test
def setup
ActionController::Base.cache_store.clear
@author = Author.new(id: 1, name: 'Steve K.')
@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]
@author.posts = [@post]

@serializer = AuthorNestedSerializer.new(@author)
@adapter = ActiveModel::Serializer::Adapter::Json.new(@serializer)
end

def test_has_many
assert_equal({
id: 1, name: 'Steve K.',
posts: [{
id: 1, title: 'New Post', body: 'Body',
comments: [
{id: 1, body: 'ZOMG A COMMENT'},
{id: 2, body: 'ZOMG ANOTHER COMMENT'}
]
}]
}, @adapter.serializable_hash(limit_depth: 5)[:author])
end

def test_limit_depth
assert_raises(StandardError) do
@adapter.serializable_hash(limit_depth: 1, check_depth_strategy: :fail)
end
end

def test_trim_strategy
assert_equal({
id: 1, name: 'Steve K.',
posts: [{
id: 1, title: 'New Post', body: 'Body',
}]
}, @adapter.serializable_hash(limit_depth: 1, check_depth_strategy: :trim)[:author])
end

def test_pass_strategy
assert_equal({
id: 1, name: 'Steve K.',
posts: [{
id: 1, title: 'New Post', body: 'Body',
comments: [
{id: 1, body: 'ZOMG A COMMENT'},
{id: 2, body: 'ZOMG ANOTHER COMMENT'}
]
}]
}, @adapter.serializable_hash(limit_depth: 1, check_depth_strategy: :pass)[:author])
end

def test_flatten_json
adapter = ActiveModel::Serializer::Adapter::FlattenJson.new(@serializer)
assert_equal({
id: 1, name: 'Steve K.',
posts: [{
id: 1, title: 'New Post', body: 'Body',
comments: [
{id: 1, body: 'ZOMG A COMMENT'},
{id: 2, body: 'ZOMG ANOTHER COMMENT'}
]
}]
}, adapter.serializable_hash(limit_depth: 5))
end
end
end
end
end
15 changes: 15 additions & 0 deletions test/fixtures/poro.rb
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,18 @@ def json_key
raise StandardError, 'Intentional error for rescue_from test'
end
end

CommentNestedSerializer = Class.new(ActiveModel::Serializer) do
attributes :body, :id
end

PostNestedSerializer = Class.new(ActiveModel::Serializer) do
attributes :title, :body, :id
has_many :comments, serializer: CommentNestedSerializer
end

AuthorNestedSerializer = Class.new(ActiveModel::Serializer) do
attributes :id, :name
has_many :posts, serializer: PostNestedSerializer
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joaomdmoura I think we should start following some convention for associating poros with tests. I think it's becoming append-only...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a suggestion.
In my projects, I am doing like this way:

class AuthorSerializer < ActiveModel::Serializer
  attributes :id, :name

  class ForShow < self
    has_many :posts
  end
end

class PostSerializer < ActiveModel::Serializer
  attributes :id, :title, :body

  class ForShow < self
    has_many :comments
  end
end

and so on.

I define default serializers not to have any relations.
And for specific cases, like showing details, define serializers as nested classes.

When I use them in a controller, I write like:

def show
  ...
  render json: @post, serializer: PostSerializer::ForShow
end

Like this way, I can easily avoid unexpected circular relations because default serializers has no associations.
And scoping with default serializer classes make it clear which serializer is for which model.
Additionally, deriving detailed serializers from default ones saves us from re-defining basic attributes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

works for me