Skip to content
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

Add :except as an option to Adapter::Attributes #1538

Closed
wants to merge 1 commit 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
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.
  • Loading branch information
Empact authored and Noah Silas committed May 25, 2016
commit bc07945b490c42fc8dc7ee773870401a5de84488
13 changes: 13 additions & 0 deletions docs/general/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,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):
Expand Down
1 change: 1 addition & 0 deletions docs/general/serializers.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Where:
- `unless:`
- `virtual_value:`
- `polymorphic:` defines if polymorphic relation type should be nested in serialized association.
- `except`: Specify attributes or associations to be ommitted from serialization
- optional: `&block` is a context that returns the association's attributes.
- prevents `association_name` method from being called.
- return value of block is used as the association value.
Expand Down
6 changes: 4 additions & 2 deletions lib/active_model/serializer/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Copy link
Member

Choose a reason for hiding this comment

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

usually only and except are mutually exclusive. If I'm reading this correctly, they're not, here, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, here both are applied as filters. only: [:a, :b], except: [:b] is equivalent to only: [:a]. This seems reasonable to me, although I can imagine how someone could be confused by it. Should this raise if we receive both?

Copy link
Member

Choose a reason for hiding this comment

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

Do you think this can be made to work with the if/unless in #1403 ? That seems to cover this case except for having options to pass in from the renderer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I follow; if/unless seems like it can prevent the entire association from being included/rendered, where except will still render the association but exclude some specific fields.

Copy link
Member

Choose a reason for hiding this comment

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

I mean, I like the idea of accepting requested options and excluded options, but this could be done, with a little more effort, via attribute unless and instance_options.

The PR is great. I'm just uncertain if I want it as a new feature to maintain, or if it's a first class behavior... Perhaps some other @rails-api/ams @rails-api/commit maintainers could chime in

Copy link
Member

Choose a reason for hiding this comment

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

It can be inferred, might be nice to make it explicit

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah, at least, to prevent PRs for things that can already be achieved.
Is that something you want to do, @bf4 ?

Copy link

Choose a reason for hiding this comment

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

@bf4 I was really looking for something like this and I have no idea how to achieve it otherwise, could you please show an example of how to do it using attribute unless and instance_options

Copy link
Member

Choose a reason for hiding this comment

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

Sure amr

Could you share your ams vesion from the gemfile.lock, your serializer,
your render command, and your desired output?

On Mon, Mar 7, 2016 at 10:21 AM Amr Noman [email protected] wrote:

In lib/active_model/serializer/attributes.rb
#1538 (comment)
:

@@ -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])
    

@bf4 https://github.com/bf4 I was really looking for something like
this and I have no idea how to achieve it otherwise, could you please show
an example of how to do it using attribute unless and instance_options


Reply to this email directly or view it on GitHub
https://github.com/rails-api/active_model_serializers/pull/1538/files#r55227776
.

Copy link

Choose a reason for hiding this comment

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

@bf4
active_model_serializers (0.10.0.rc4)
Say I have a Chatroom serializer:

class ChatroomSerializer < ActiveModel::Serializer
  attributes :id, :name, :description, :room_type, :subscribed
  has_many :subscribers
  has_many :visitors

  def subscribed
    object.subscribers.include? current_user
  end
end

Now sometimes the client may only want basic attributes for the chatroom like name, description, room_type...etc. and doesn't care about the associations, so I'd like to do something like this in my actions:
render json: @chatroom, except: [:subscribers, :visitors]

or even something like this:
render json: @chatroom, except: params[:except_attrs]

I know I can specify a specific serializer to render, but I don't want to create a separate serializer for each small variation, am I making sense?

@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
Expand Down
11 changes: 8 additions & 3 deletions lib/active_model_serializers/adapter/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,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

Expand All @@ -45,7 +47,8 @@ def relationship_value_for(association, options)
return unless association.serializer && association.serializer.object

opts = instance_options.merge(include: @include_tree[association.key])
relationship_value = Attributes.new(association.serializer, opts).serializable_hash(options)
hash_opts = options.merge(except: association.options[:except])
relationship_value = Attributes.new(association.serializer, opts).serializable_hash(hash_opts)

if association.options[:polymorphic] && relationship_value
polymorphic_type = association.serializer.object.class.name.underscore
Expand All @@ -63,12 +66,14 @@ def cache_attributes
end

def resource_object_for(options)
fields = options.fetch(:fields, {})
fields = fields.merge(except: options[:except]) if options[:except]
if serializer.class.cache_enabled?
@cached_attributes.fetch(serializer.cache_key(self)) do
serializer.cached_fields(options[:fields], self)
serializer.cached_fields(fields, self)
end
else
serializer.cached_fields(options[:fields], self)
serializer.cached_fields(fields, self)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/active_model_serializers/adapter/json_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ def process_relationship(serializer, include_tree)
# foo: 'bar'
# }
def attributes_for(serializer, fields)
serializer.attributes(fields).except(:id)
serializer.attributes(only: fields, except: :id)
end

# {http://jsonapi.org/format/#document-resource-objects Document Resource Objects}
Expand Down
19 changes: 19 additions & 0 deletions test/action_controller/serialization_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,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
Expand Down Expand Up @@ -464,6 +475,14 @@ def test_render_event_is_emmited
ensure
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
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
41 changes: 41 additions & 0 deletions test/serializers/associations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,47 @@ def test_illegal_conditional_associations

assert_match(/:if should be a Symbol, String or Proc/, exception.message)
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
Expand Down