From e4a56563e454d06351b3c53f172c9653e1a3ac70 Mon Sep 17 00:00:00 2001 From: Ben Woosley Date: Mon, 18 Jan 2016 11:56:20 -0800 Subject: [PATCH] Add :except as an option to Adapter::Attributes This supports passing `except:` as an option to `render` or as an option to `has_many`, etc. declrations. This is useful when avoiding circular includes. The `except` name was chosen to mirror behavior found in the 0.8x branch. --- docs/general/adapters.md | 13 ++++++ docs/general/serializers.md | 7 ++++ .../serializer/adapter/attributes.rb | 9 +++- .../serializer/adapter/json_api.rb | 2 +- lib/active_model/serializer/attributes.rb | 6 ++- test/action_controller/serialization_test.rb | 19 +++++++++ test/serializers/associations_test.rb | 41 +++++++++++++++++++ 7 files changed, 92 insertions(+), 5 deletions(-) diff --git a/docs/general/adapters.md b/docs/general/adapters.md index 262c4418f..1db2c95f8 100644 --- a/docs/general/adapters.md +++ b/docs/general/adapters.md @@ -97,6 +97,19 @@ It could be combined, like above, with other paths in any combination desired. render json: @posts, include: 'author.comments.**' ``` +#### Excluded + +Sometimes you want to omit a specific field or association during serialization. +You can use the `except` option for this: + +```ruby + render json: @posts, include: '*', except: :author +``` + +This is particularly helpful if you are using the recursive include wildstar +(`**`), as it can lead to infinite recursion when you have associations that +can be traversed in a cycle. + ##### Security Considerations Since the included options may come from the query params (i.e. user-controller): diff --git a/docs/general/serializers.md b/docs/general/serializers.md index 65ccaa1a7..33f59c5fc 100644 --- a/docs/general/serializers.md +++ b/docs/general/serializers.md @@ -76,6 +76,13 @@ def blog end ``` +#### Association Options +- `key`: Sets the key name to assign in the serialized output for this + association +- `serializer`: Choose an explicit serializer to use for associated objects +- `except`: Select attributes or associations belonging to the associated + objects that should be omitted from serialization. + ### Caching #### ::cache diff --git a/lib/active_model/serializer/adapter/attributes.rb b/lib/active_model/serializer/adapter/attributes.rb index 657cd1f17..b5fc4cd64 100644 --- a/lib/active_model/serializer/adapter/attributes.rb +++ b/lib/active_model/serializer/adapter/attributes.rb @@ -65,7 +65,9 @@ def serializable_hash_for_single_resource(options) def resource_relationships(options) relationships = {} + excepts = Array(options[:except]) serializer.associations(@include_tree).each do |association| + next if excepts.include?(association.key) relationships[association.key] = relationship_value_for(association, options) end @@ -77,7 +79,8 @@ def relationship_value_for(association, options) return unless association.serializer && association.serializer.object opts = instance_options.merge(include: @include_tree[association.key]) - Attributes.new(association.serializer, opts).serializable_hash(options) + hash_opts = options.merge(except: association.options[:except]) + Attributes.new(association.serializer, opts).serializable_hash(hash_opts) end # no-op: Attributes adapter does not include meta data, because it does not support root. @@ -90,7 +93,9 @@ def resource_object_for(options) cached_attributes(cached_serializer) do cached_serializer.cache_check(self) do - serializer.attributes(options[:fields]) + serializer.attributes( + only: options[:fields], + except: options[:except]) end end end diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index cfe4e04dc..e98175b56 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -130,7 +130,7 @@ def process_relationship(serializer, include_tree) end def attributes_for(serializer, fields) - serializer.attributes(fields).except(:id) + serializer.attributes(only: fields, except: :id) end def resource_object_for(serializer) diff --git a/lib/active_model/serializer/attributes.rb b/lib/active_model/serializer/attributes.rb index 11d39c4b2..d73f00d43 100644 --- a/lib/active_model/serializer/attributes.rb +++ b/lib/active_model/serializer/attributes.rb @@ -14,10 +14,12 @@ module Attributes # Return the +attributes+ of +object+ as presented # by the serializer. - def attributes(requested_attrs = nil, reload = false) + def attributes(options = {}, reload = false) + requested_attrs = options[:only] + excepts = Array(options[:except]) @attributes = nil if reload @attributes ||= self.class._attributes_data.each_with_object({}) do |(key, attr), hash| - next if attr.excluded?(self) + next if attr.excluded?(self) || excepts.include?(key) next unless requested_attrs.nil? || requested_attrs.include?(key) hash[key] = attr.value(self) end diff --git a/test/action_controller/serialization_test.rb b/test/action_controller/serialization_test.rb index 7e6ea6ccd..3a6d41b59 100644 --- a/test/action_controller/serialization_test.rb +++ b/test/action_controller/serialization_test.rb @@ -141,6 +141,17 @@ def render_fragment_changed_object_with_relationship render json: like end + + def render_object_except + blog = Blog.new(id: 1, name: 'Blogariffic') + blog.articles = [ + Post.new(id: 1, title: 'Hello', body: 'world'), + Post.new(id: 2, title: 'Moby Dick', body: 'Call me Ishmael.') + ] + blog.writer = Author.new(id: 1, name: 'Joao Moura.') + + render json: blog, except: [:articles, :name] + end end tests ImplicitSerializationTestController @@ -463,6 +474,14 @@ def test_render_event_is_emmited assert_equal 'render.active_model_serializers', @name end + + def test_render_object_except + get :render_object_except + assert_equal( + { id: 1, writer: { id: 1, name: 'Joao Moura.' } }.to_json, + @response.body + ) + end end end end diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index f62da8b81..c54fb81ab 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -261,6 +261,47 @@ def false assert_equal(expected, hash) end + + def test_association_except + # `except` can take an array + comment_serializer = Class.new(ActiveModel::Serializer) do + attributes :id, :body + belongs_to :author, except: [:posts, :roles, :bio] + belongs_to :post + end + + # `except` can take a symbol + post_serializer = Class.new(ActiveModel::Serializer) do + attributes :id, :title, :body + has_many :comments, except: :post, serializer: comment_serializer + end + + # Circular dependency created: + # - post has_many comments + # - comment belongs_to post + # excluding the "post" association on comment resolves it when + # we are including nested associations + + author = Author.new(id: 1, name: 'Alice') + post = Post.new(id: 7, title: 'Do work', body: 'work work work') + post.comments = [ + Comment.new(post: post, author: author, id: 2, body: 'I agree'), + Comment.new(post: post, author: author, id: 3, body: 'Right') + ] + + hash = serializable(post, serializer: post_serializer, include: '**').serializable_hash + + expected = { + id: 7, + title: 'Do work', + body: 'work work work', + comments: [ + { id: 2, body: 'I agree', author: { id: 1, name: 'Alice' } }, + { id: 3, body: 'Right', author: { id: 1, name: 'Alice' } } + ] + } + assert_equal(expected, hash) + end end end end