diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b2df5e74..177586563 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Breaking changes: Features: +- [#2362](https://github.com/rails-api/active_model_serializers/pull/2362) Add `allow_wildcard` configuration option (@Borzik) + Fixes: - [#2344](https://github.com/rails-api/active_model_serializers/pull/2344) Fixes #2341 introduced since #2223 (@wasifhossain) diff --git a/docs/general/configuration_options.md b/docs/general/configuration_options.md index a07d71ee8..c16ec5a25 100644 --- a/docs/general/configuration_options.md +++ b/docs/general/configuration_options.md @@ -62,6 +62,18 @@ What relationships to serialize by default. Default: `'*'`, which includes one objects. See [includes](adapters.md#included) for more info. +##### allow_wildcard + +Enable wildcard `include` directives (`*`, `**`). + +Possible values: + +- `true` (default) +- `false` + +When `false`, nested associations to be included must be explicitly specified. Note: this will override default_includes configuration as `*` include will no longer be accepted. + + ##### serializer_lookup_chain Configures how serializers are searched for. By default, the lookup chain is diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 605d9dc5e..ddabb4d6b 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -106,7 +106,7 @@ def self.include_directive_from_options(options) if options[:include_directive] options[:include_directive] elsif options[:include] - JSONAPI::IncludeDirective.new(options[:include], allow_wildcard: true) + JSONAPI::IncludeDirective.new(options[:include], allow_wildcard: config.allow_wildcard) else ActiveModelSerializers.default_include_directive end @@ -135,6 +135,7 @@ def config.array_serializer end config.default_includes = '*' + config.allow_wildcard = true config.adapter = :attributes config.key_transform = nil config.jsonapi_pagination_links_enabled = true diff --git a/lib/active_model_serializers.rb b/lib/active_model_serializers.rb index da9f9a040..1a9bbaf4e 100644 --- a/lib/active_model_serializers.rb +++ b/lib/active_model_serializers.rb @@ -40,7 +40,7 @@ def self.location_of_caller # Memoized default include directive # @return [JSONAPI::IncludeDirective] def self.default_include_directive - @default_include_directive ||= JSONAPI::IncludeDirective.new(config.default_includes, allow_wildcard: true) + @default_include_directive ||= JSONAPI::IncludeDirective.new(config.default_includes, allow_wildcard: config.allow_wildcard) end def self.silence_warnings diff --git a/lib/active_model_serializers/adapter/json_api.rb b/lib/active_model_serializers/adapter/json_api.rb index 83c75ea82..98f390888 100644 --- a/lib/active_model_serializers/adapter/json_api.rb +++ b/lib/active_model_serializers/adapter/json_api.rb @@ -52,7 +52,7 @@ def self.fragment_cache(cached_hash, non_cached_hash, root = true) def initialize(serializer, options = {}) super - @include_directive = JSONAPI::IncludeDirective.new(options[:include], allow_wildcard: true) + @include_directive = JSONAPI::IncludeDirective.new(options[:include], allow_wildcard: ActiveModelSerializers.config.allow_wildcard) @fieldset = options[:fieldset] || ActiveModel::Serializer::Fieldset.new(options.delete(:fields)) end @@ -452,7 +452,7 @@ def data_for(serializer, include_slice) def relationships_for(serializer, requested_associations, include_slice) include_directive = JSONAPI::IncludeDirective.new( requested_associations, - allow_wildcard: true + allow_wildcard: ActiveModelSerializers.config.allow_wildcard ) serializer.associations(include_directive, include_slice).each_with_object({}) do |association, hash| hash[association.key] = Relationship.new(serializer, instance_options, association).as_json diff --git a/test/action_controller/json/include_test.rb b/test/action_controller/json/include_test.rb index d4268721a..86cfca118 100644 --- a/test/action_controller/json/include_test.rb +++ b/test/action_controller/json/include_test.rb @@ -9,6 +9,8 @@ class IncludeTest < ActionController::TestCase INCLUDE_STRING = 'posts.comments'.freeze INCLUDE_HASH = { posts: :comments }.freeze DEEP_INCLUDE = 'posts.comments.author'.freeze + WILDCARD_INCLUDE = '*'.freeze + DEEP_WILDCARD_INCLUDE = '**'.freeze class IncludeTestController < ActionController::Base def setup_data @@ -57,6 +59,16 @@ def render_resource_with_deep_include render json: @author, include: DEEP_INCLUDE, adapter: :json end + def render_resource_with_wildcard_include + setup_data + render json: @author, include: WILDCARD_INCLUDE, adapter: :json + end + + def render_resource_with_deep_wildcard_include + setup_data + render json: @author, include: DEEP_WILDCARD_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. @@ -114,6 +126,48 @@ def test_render_resource_with_deep_include assert_equal(expected_deep_include_response, response) end + def test_render_resource_with_wildcard_include + get :render_resource_with_wildcard_include + + response = JSON.parse(@response.body) + + assert_equal(expected_wildcard_include_response, response) + end + + def test_render_resource_with_wildcard_include_with_wildcards_disabled + with_wildcards_disabled do + get :render_resource_with_wildcard_include + end + + expected = { + 'author' => { + 'id' => 1, + 'name' => 'Steve K.' + } + } + + response = JSON.parse(@response.body) + + assert_equal(expected, response) + end + + def test_render_resource_with_deep_wildcard_include_with_wildcards_disabled + with_wildcards_disabled do + get :render_resource_with_deep_wildcard_include + end + + expected = { + 'author' => { + 'id' => 1, + 'name' => 'Steve K.' + } + } + + response = JSON.parse(@response.body) + + assert_equal(expected, response) + end + def test_render_with_empty_default_includes with_default_includes '' do get :render_without_include @@ -176,6 +230,22 @@ def test_render_with_includes_overrides_default_includes private + def expected_wildcard_include_response + { + 'author' => { + 'id' => 1, + 'name' => 'Steve K.', + 'posts' => [ + { + 'id' => 42, 'title' => 'New Post', 'body' => 'Body' + } + ], + 'roles' => [], + 'bio' => {} + } + } + end + def expected_include_response { 'author' => { @@ -238,6 +308,14 @@ def with_default_includes(include_directive) clear_include_directive_cache end + def with_wildcards_disabled + original = ActiveModelSerializers.config.allow_wildcard + ActiveModelSerializers.config.allow_wildcard = false + yield + ensure + ActiveModelSerializers.config.allow_wildcard = original + end + def clear_include_directive_cache ActiveModelSerializers .instance_variable_set(:@default_include_directive, nil) diff --git a/test/action_controller/json_api/linked_test.rb b/test/action_controller/json_api/linked_test.rb index 69d8974d7..85782b340 100644 --- a/test/action_controller/json_api/linked_test.rb +++ b/test/action_controller/json_api/linked_test.rb @@ -150,6 +150,23 @@ def test_render_resource_with_nested_has_many_include assert_equal expected_linked, response['included'] end + def test_render_resource_with_nested_has_many_include_with_wildcards_disabled + ActiveModel::Serializer.config.allow_wildcard = false + get '/render_resource_with_nested_has_many_include_wildcard' + ActiveModel::Serializer.config.allow_wildcard = true + response = JSON.parse(@response.body) + expected_linked = [ + { + 'id' => '1', + 'type' => 'authors', + 'attributes' => { + 'name' => 'Steve K.' + } + } + ] + assert_equal expected_linked, response['included'] + end + def test_render_resource_with_include_of_custom_key_by_original get '/render_resource_with_include_of_custom_key_by_original' response = JSON.parse(@response.body)