From 8c18d18cdb6defc1a892c814f33cf732ed4d7a20 Mon Sep 17 00:00:00 2001 From: Ben Woosley Date: Tue, 12 Jan 2016 12:05:11 -0800 Subject: [PATCH] Add default_includes configuration This is useful to set application-wide default behavior - e.g. in previous versions of AMS the default behavior was to serialize the full object graph by default - equivalent to the '**' include tree. Currently just the global setting, but I think this could also work on a per-serializer basis, with more attention. --- CHANGELOG.md | 1 + docs/general/configuration_options.md | 4 +- lib/active_model/serializer/associations.rb | 7 +- lib/active_model/serializer/configuration.rb | 1 + lib/active_model_serializers.rb | 7 + .../adapter/attributes.rb | 7 +- test/action_controller/json/include_test.rb | 143 ++++++++++++++---- 7 files changed, 132 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2a8e2bd..f08e7ab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Breaking changes: Features: +- [#1426](https://github.com/rails-api/active_model_serializers/pull/1426) Add ActiveModelSerializers.config.default_includes (@empact) Fixes: - [#1710](https://github.com/rails-api/active_model_serializers/pull/1710) Prevent association loading when `include_data` option diff --git a/docs/general/configuration_options.md b/docs/general/configuration_options.md index 82719fb2..a8561554 100644 --- a/docs/general/configuration_options.md +++ b/docs/general/configuration_options.md @@ -52,10 +52,12 @@ Each adapter has a default key transform configured: `config.key_transform` is a global override of the adapter default. Adapters still prefer the render option `:key_transform` over this setting. +##### default_includes +What relationships to serialize by default. Default: `'*'`, which includes one level of related +objects. See [includes](adapters.md#included) for more info. ## JSON API - ##### jsonapi_resource_type Sets whether the [type](http://jsonapi.org/format/#document-resource-identifier-objects) diff --git a/lib/active_model/serializer/associations.rb b/lib/active_model/serializer/associations.rb index 7d87156e..78448ea7 100644 --- a/lib/active_model/serializer/associations.rb +++ b/lib/active_model/serializer/associations.rb @@ -10,8 +10,6 @@ module ActiveModel module Associations extend ActiveSupport::Concern - DEFAULT_INCLUDE_TREE = ActiveModel::Serializer::IncludeTree.from_string('*') - included do with_options instance_writer: false, instance_reader: true do |serializer| serializer.class_attribute :_reflections @@ -80,10 +78,11 @@ module ActiveModel end end - # @param [IncludeTree] include_tree (defaults to all associations when not provided) + # @param [IncludeTree] include_tree (defaults to the + # default_includes config value when not provided) # @return [Enumerator] # - def associations(include_tree = DEFAULT_INCLUDE_TREE) + def associations(include_tree = ActiveModelSerializers.default_include_tree) return unless object Enumerator.new do |y| diff --git a/lib/active_model/serializer/configuration.rb b/lib/active_model/serializer/configuration.rb index 1553e632..7ee45fb4 100644 --- a/lib/active_model/serializer/configuration.rb +++ b/lib/active_model/serializer/configuration.rb @@ -19,6 +19,7 @@ module ActiveModel collection_serializer end + config.default_includes = '*' config.adapter = :attributes config.jsonapi_resource_type = :plural config.jsonapi_version = '1.0' diff --git a/lib/active_model_serializers.rb b/lib/active_model_serializers.rb index 6241fac1..8e7b5fa2 100644 --- a/lib/active_model_serializers.rb +++ b/lib/active_model_serializers.rb @@ -31,6 +31,13 @@ module ActiveModelSerializers [file, lineno] end + # Memoized default include tree + # @return [ActiveModel::Serializer::IncludeTree] + def self.default_include_tree + @default_include_tree ||= ActiveModel::Serializer::IncludeTree + .from_include_args(config.default_includes) + end + require 'active_model/serializer/version' require 'active_model/serializer' require 'active_model/serializable_resource' diff --git a/lib/active_model_serializers/adapter/attributes.rb b/lib/active_model_serializers/adapter/attributes.rb index 8fc8441e..e30d2efb 100644 --- a/lib/active_model_serializers/adapter/attributes.rb +++ b/lib/active_model_serializers/adapter/attributes.rb @@ -3,8 +3,13 @@ module ActiveModelSerializers class Attributes < Base def initialize(serializer, options = {}) super - @include_tree = ActiveModel::Serializer::IncludeTree.from_include_args(options[:include] || '*') @cached_attributes = options[:cache_attributes] || {} + @include_tree = + if options[:include] + ActiveModel::Serializer::IncludeTree.from_include_args(options[:include]) + else + ActiveModelSerializers.default_include_tree + end end def serializable_hash(options = nil) diff --git a/test/action_controller/json/include_test.rb b/test/action_controller/json/include_test.rb index ac5ab25e..9d0512d1 100644 --- a/test/action_controller/json/include_test.rb +++ b/test/action_controller/json/include_test.rb @@ -4,6 +4,10 @@ module ActionController module Serialization class Json class IncludeTest < ActionController::TestCase + INCLUDE_STRING = 'posts.comments'.freeze + INCLUDE_HASH = { posts: :comments }.freeze + DEEP_INCLUDE = 'posts.comments.author'.freeze + class IncludeTestController < ActionController::Base def setup_data ActionController::Base.cache_store.clear @@ -38,17 +42,28 @@ module ActionController def render_resource_with_include_hash setup_data - render json: @author, include: { posts: :comments }, adapter: :json + render json: @author, include: INCLUDE_HASH, adapter: :json end def render_resource_with_include_string setup_data - render json: @author, include: 'posts.comments', adapter: :json + render json: @author, include: INCLUDE_STRING, adapter: :json end def render_resource_with_deep_include setup_data - render json: @author, include: 'posts.comments.author', adapter: :json + render json: @author, include: DEEP_INCLUDE, adapter: :json + end + + def render_without_recursive_relationships + # testing recursive includes ('**') can't have any cycles in the + # relationships, or we enter an infinite loop. + author = Author.new(id: 11, name: 'Jane Doe') + post = Post.new(id: 12, title: 'Hello World', body: 'My first post') + comment = Comment.new(id: 13, body: 'Commentary') + author.posts = [post] + post.comments = [comment] + render json: author end end @@ -77,34 +92,90 @@ module ActionController def test_render_resource_with_include_hash get :render_resource_with_include_hash response = JSON.parse(@response.body) - expected = { - 'author' => { - 'id' => 1, - 'name' => 'Steve K.', - 'posts' => [ - { - 'id' => 42, 'title' => 'New Post', 'body' => 'Body', - 'comments' => [ - { - 'id' => 1, 'body' => 'ZOMG A COMMENT' - }, - { - 'id' => 2, 'body' => 'ZOMG ANOTHER COMMENT' - } - ] - } - ] - } - } - assert_equal(expected, response) + assert_equal(expected_include_response, response) end def test_render_resource_with_include_string get :render_resource_with_include_string response = JSON.parse(@response.body) - expected = { + + assert_equal(expected_include_response, response) + end + + def test_render_resource_with_deep_include + get :render_resource_with_deep_include + + response = JSON.parse(@response.body) + + assert_equal(expected_deep_include_response, response) + end + + def test_render_with_empty_default_includes + with_default_includes '' do + get :render_without_include + response = JSON.parse(@response.body) + expected = { + 'author' => { + 'id' => 1, + 'name' => 'Steve K.' + } + } + assert_equal(expected, response) + end + end + + def test_render_with_recursive_default_includes + with_default_includes '**' do + get :render_without_recursive_relationships + response = JSON.parse(@response.body) + + expected = { + 'id' => 11, + 'name' => 'Jane Doe', + 'roles' => nil, + 'bio' => nil, + 'posts' => [ + { + 'id' => 12, + 'title' => 'Hello World', + 'body' => 'My first post', + 'comments' => [ + { + 'id' => 13, + 'body' => 'Commentary', + 'post' => nil, # not set to avoid infinite recursion + 'author' => nil, # not set to avoid infinite recursion + } + ], + 'blog' => { + 'id' => 999, + 'name' => 'Custom blog', + 'writer' => nil, + 'articles' => nil + }, + 'author' => nil # not set to avoid infinite recursion + } + ] + } + assert_equal(expected, response) + end + end + + def test_render_with_includes_overrides_default_includes + with_default_includes '' do + get :render_resource_with_include_hash + response = JSON.parse(@response.body) + + assert_equal(expected_include_response, response) + end + end + + private + + def expected_include_response + { 'author' => { 'id' => 1, 'name' => 'Steve K.', @@ -123,15 +194,10 @@ module ActionController ] } } - - assert_equal(expected, response) end - def test_render_resource_with_deep_include - get :render_resource_with_deep_include - - response = JSON.parse(@response.body) - expected = { + def expected_deep_include_response + { 'author' => { 'id' => 1, 'name' => 'Steve K.', @@ -158,8 +224,21 @@ module ActionController ] } } + end - assert_equal(expected, response) + def with_default_includes(include_tree) + original = ActiveModelSerializers.config.default_includes + ActiveModelSerializers.config.default_includes = include_tree + clear_include_tree_cache + yield + ensure + ActiveModelSerializers.config.default_includes = original + clear_include_tree_cache + end + + def clear_include_tree_cache + ActiveModelSerializers + .instance_variable_set(:@default_include_tree, nil) end end end