diff --git a/lib/active_model/serializer/adapter/flatten_json.rb b/lib/active_model/serializer/adapter/flatten_json.rb index 7ed570349..10848a03a 100644 --- a/lib/active_model/serializer/adapter/flatten_json.rb +++ b/lib/active_model/serializer/adapter/flatten_json.rb @@ -2,14 +2,12 @@ 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 @@ -17,3 +15,4 @@ def include_meta(json) end end end + diff --git a/lib/active_model/serializer/adapter/json.rb b/lib/active_model/serializer/adapter/json.rb index 58704b56e..1f1d8c081 100644 --- a/lib/active_model/serializer/adapter/json.rb +++ b/lib/active_model/serializer/adapter/json.rb @@ -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 + case @check_depth_strategy + when :pass + false + when :fail + fail 'Too deep associations.' + when :trim + true + end + else + false + end end - end end end diff --git a/test/adapter/json/nested_json_test.rb b/test/adapter/json/nested_json_test.rb new file mode 100644 index 000000000..20f759a2f --- /dev/null +++ b/test/adapter/json/nested_json_test.rb @@ -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 diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb index 2b281049c..c2317255c 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -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 +