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

Conditional attributes/associations (if/unless) #1403

Merged
merged 6 commits into from
Jan 13, 2016
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Breaking changes:

Features:

- [#1403](https://github.com/rails-api/active_model_serializers/pull/1403) Add support for if/unless on attributes/associations (@beauby)
- [#1248](https://github.com/rails-api/active_model_serializers/pull/1248) Experimental: Add support for JSON API deserialization (@beauby)
- [#1378](https://github.com/rails-api/active_model_serializers/pull/1378) Change association blocks
to be evaluated in *serializer* scope, rather than *association* scope. (@bf4)
Expand Down
1 change: 1 addition & 0 deletions lib/active_model/serializer/associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def associations(include_tree = DEFAULT_INCLUDE_TREE)

Enumerator.new do |y|
self.class._reflections.each do |reflection|
next if reflection.excluded?(self)
key = reflection.options.fetch(:key, reflection.name)
next unless include_tree.key?(key)
y.yield reflection.build_association(self, instance_options)
Expand Down
28 changes: 20 additions & 8 deletions lib/active_model/serializer/attribute.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
require 'active_model/serializer/field'

module ActiveModel
class Serializer
Attribute = Struct.new(:name, :block) do
def value(serializer)
if block
serializer.instance_eval(&block)
else
serializer.read_attribute_for_serialization(name)
end
end
# Holds all the meta-data about an attribute as it was specified in the
# ActiveModel::Serializer class.
#
# @example
# class PostSerializer < ActiveModel::Serializer
# attribute :content
# attribute :name, key: :title
# attribute :email, key: :author_email, if: :user_logged_in?
# attribute :preview do
# truncate(object.content)
# end
#
# def user_logged_in?
# current_user.logged_in?
# end
# end
#
class Attribute < Field
end
end
end
3 changes: 2 additions & 1 deletion lib/active_model/serializer/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module Attributes
def attributes(requested_attrs = nil, reload = false)
@attributes = nil if reload
@attributes ||= self.class._attributes_data.each_with_object({}) do |(key, attr), hash|
next if attr.excluded?(self)
next unless requested_attrs.nil? || requested_attrs.include?(key)
hash[key] = attr.value(self)
end
Expand Down Expand Up @@ -54,7 +55,7 @@ def attributes(*attrs)
# end
def attribute(attr, options = {}, &block)
key = options.fetch(:key, attr)
_attributes_data[key] = Attribute.new(attr, block)
_attributes_data[key] = Attribute.new(attr, options, block)
end

# @api private
Expand Down
56 changes: 56 additions & 0 deletions lib/active_model/serializer/field.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
module ActiveModel
class Serializer
# Holds all the meta-data about a field (i.e. attribute or association) as it was
Copy link
Member

Choose a reason for hiding this comment

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

grammar: s/i.e./e.g.

reason: meaning is to change 'that is' to 'for example'

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 actually meant i.e., because I'm just defining what "field" means.

Copy link
Member

Choose a reason for hiding this comment

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

still think it should be e.g., but since it's intentional on your part and it's super unimportant, ok to merge when green

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed that it's not super important, but as I like grammar as well: a field is defined as "either an attribute or an association".

# specified in the ActiveModel::Serializer class.
# Notice that the field block is evaluated in the context of the serializer.
Field = Struct.new(:name, :options, :block) do
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Kept it as a struct because tests in test/serializers/association_macros_test.rb made use of the comparability. Wouldn't mind modifying those in a subsequent PR.

# Compute the actual value of a field for a given serializer instance.
# @param [Serializer] The serializer instance for which the value is computed.
# @return [Object] value
#
# @api private
#
def value(serializer)
if block
serializer.instance_eval(&block)
else
serializer.read_attribute_for_serialization(name)
end
end

# Decide whether the field should be serialized by the given serializer instance.
# @param [Serializer] The serializer instance
# @return [Bool]
#
# @api private
#
def excluded?(serializer)
case condition_type
when :if
!serializer.public_send(condition)
when :unless
serializer.public_send(condition)
else
false
end
end

private

def condition_type
@condition_type ||=
if options.key?(:if)
:if
elsif options.key?(:unless)
:unless
else
:none
end
end

def condition
options[condition_type]
end
end
end
end
25 changes: 12 additions & 13 deletions lib/active_model/serializer/reflection.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
require 'active_model/serializer/field'

module ActiveModel
class Serializer
# Holds all the meta-data about an association as it was specified in the
# ActiveModel::Serializer class.
#
# @example
# class PostSerializer < ActiveModel::Serializer
# class PostSerializer < ActiveModel::Serializer
# has_one :author, serializer: AuthorSerializer
# has_many :comments
# has_many :comments, key: :last_comments do
# object.comments.last(1)
# end
# end
# has_many :secret_meta_data, if: :is_admin?
#
# def is_admin?
# current_user.admin?
# end
Copy link
Member

Choose a reason for hiding this comment

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

I like how you slipped the scope in there :) ref: #1252

# end
#
# Notice that the association block is evaluated in the context of the serializer.
# Specifically, the association 'comments' is evaluated two different ways:
# 1) as 'comments' and named 'comments'.
# 2) as 'object.comments.last(1)' and named 'last_comments'.
Expand All @@ -21,20 +27,13 @@ class Serializer
# # [
# # HasOneReflection.new(:author, serializer: AuthorSerializer),
# # HasManyReflection.new(:comments)
# # HasManyReflection.new(:comments, { key: :last_comments }, #<Block>)
# # HasManyReflection.new(:secret_meta_data, { if: :is_admin? })
# # ]
#
# So you can inspect reflections in your Adapters.
#
Reflection = Struct.new(:name, :options, :block) do
# @api private
def value(instance)
if block
instance.instance_eval(&block)
else
instance.read_attribute_for_serialization(name)
end
end

class Reflection < Field
# Build association. This method is used internally to
# build serializer's association by its reflection.
#
Expand Down
23 changes: 23 additions & 0 deletions test/serializers/associations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,29 @@ def test_associations_namespaced_resources
end
end
end

def test_conditional_associations
serializer = Class.new(ActiveModel::Serializer) do
belongs_to :if_assoc_included, if: :true
belongs_to :if_assoc_excluded, if: :false
belongs_to :unless_assoc_included, unless: :false
belongs_to :unless_assoc_excluded, unless: :true

def true
true
end

def false
false
end
end

model = ::Model.new
hash = serializable(model, serializer: serializer).serializable_hash
expected = { if_assoc_included: nil, unless_assoc_included: nil }

assert_equal(expected, hash)
end
end
end
end
Expand Down
25 changes: 24 additions & 1 deletion test/serializers/attribute_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module ActiveModel
class Serializer
class AttributeTest < ActiveSupport::TestCase
def setup
@blog = Blog.new({ id: 1, name: 'AMS Hints', type: 'stuff' })
@blog = Blog.new(id: 1, name: 'AMS Hints', type: 'stuff')
@blog_serializer = AlternateBlogSerializer.new(@blog)
end

Expand Down Expand Up @@ -95,6 +95,29 @@ def test_virtual_attribute_block

assert_equal(expected, hash)
end

def test_conditional_attributes
serializer = Class.new(ActiveModel::Serializer) do
attribute :if_attribute_included, if: :true
attribute :if_attribute_excluded, if: :false
attribute :unless_attribute_included, unless: :false
attribute :unless_attribute_excluded, unless: :true

def true
true
end

def false
false
end
end

model = ::Model.new
hash = serializable(model, serializer: serializer).serializable_hash
expected = { if_attribute_included: nil, unless_attribute_included: nil }

assert_equal(expected, hash)
Copy link
Member

Choose a reason for hiding this comment

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

we should look into if rubocop has a linter for parens around assert.. hashtag lazy comment

end
end
end
end