Skip to content

Commit 53c59a1

Browse files
committed
Merge pull request #1403 from beauby/conditional-attributes
Conditional attributes/associations (if/unless).
2 parents adaf5b8 + 2696557 commit 53c59a1

File tree

8 files changed

+139
-23
lines changed

8 files changed

+139
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Breaking changes:
1616

1717
Features:
1818

19+
- [#1403](https://github.com/rails-api/active_model_serializers/pull/1403) Add support for if/unless on attributes/associations (@beauby)
1920
- [#1248](https://github.com/rails-api/active_model_serializers/pull/1248) Experimental: Add support for JSON API deserialization (@beauby)
2021
- [#1378](https://github.com/rails-api/active_model_serializers/pull/1378) Change association blocks
2122
to be evaluated in *serializer* scope, rather than *association* scope. (@bf4)

lib/active_model/serializer/associations.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def associations(include_tree = DEFAULT_INCLUDE_TREE)
8888

8989
Enumerator.new do |y|
9090
self.class._reflections.each do |reflection|
91+
next if reflection.excluded?(self)
9192
key = reflection.options.fetch(:key, reflection.name)
9293
next unless include_tree.key?(key)
9394
y.yield reflection.build_association(self, instance_options)
Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
1+
require 'active_model/serializer/field'
2+
13
module ActiveModel
24
class Serializer
3-
Attribute = Struct.new(:name, :block) do
4-
def value(serializer)
5-
if block
6-
serializer.instance_eval(&block)
7-
else
8-
serializer.read_attribute_for_serialization(name)
9-
end
10-
end
5+
# Holds all the meta-data about an attribute as it was specified in the
6+
# ActiveModel::Serializer class.
7+
#
8+
# @example
9+
# class PostSerializer < ActiveModel::Serializer
10+
# attribute :content
11+
# attribute :name, key: :title
12+
# attribute :email, key: :author_email, if: :user_logged_in?
13+
# attribute :preview do
14+
# truncate(object.content)
15+
# end
16+
#
17+
# def user_logged_in?
18+
# current_user.logged_in?
19+
# end
20+
# end
21+
#
22+
class Attribute < Field
1123
end
1224
end
1325
end

lib/active_model/serializer/attributes.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ module Attributes
1717
def attributes(requested_attrs = nil, reload = false)
1818
@attributes = nil if reload
1919
@attributes ||= self.class._attributes_data.each_with_object({}) do |(key, attr), hash|
20+
next if attr.excluded?(self)
2021
next unless requested_attrs.nil? || requested_attrs.include?(key)
2122
hash[key] = attr.value(self)
2223
end
@@ -54,7 +55,7 @@ def attributes(*attrs)
5455
# end
5556
def attribute(attr, options = {}, &block)
5657
key = options.fetch(:key, attr)
57-
_attributes_data[key] = Attribute.new(attr, block)
58+
_attributes_data[key] = Attribute.new(attr, options, block)
5859
end
5960

6061
# @api private

lib/active_model/serializer/field.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
module ActiveModel
2+
class Serializer
3+
# Holds all the meta-data about a field (i.e. attribute or association) as it was
4+
# specified in the ActiveModel::Serializer class.
5+
# Notice that the field block is evaluated in the context of the serializer.
6+
Field = Struct.new(:name, :options, :block) do
7+
# Compute the actual value of a field for a given serializer instance.
8+
# @param [Serializer] The serializer instance for which the value is computed.
9+
# @return [Object] value
10+
#
11+
# @api private
12+
#
13+
def value(serializer)
14+
if block
15+
serializer.instance_eval(&block)
16+
else
17+
serializer.read_attribute_for_serialization(name)
18+
end
19+
end
20+
21+
# Decide whether the field should be serialized by the given serializer instance.
22+
# @param [Serializer] The serializer instance
23+
# @return [Bool]
24+
#
25+
# @api private
26+
#
27+
def excluded?(serializer)
28+
case condition_type
29+
when :if
30+
!serializer.public_send(condition)
31+
when :unless
32+
serializer.public_send(condition)
33+
else
34+
false
35+
end
36+
end
37+
38+
private
39+
40+
def condition_type
41+
@condition_type ||=
42+
if options.key?(:if)
43+
:if
44+
elsif options.key?(:unless)
45+
:unless
46+
else
47+
:none
48+
end
49+
end
50+
51+
def condition
52+
options[condition_type]
53+
end
54+
end
55+
end
56+
end

lib/active_model/serializer/reflection.rb

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1+
require 'active_model/serializer/field'
2+
13
module ActiveModel
24
class Serializer
35
# Holds all the meta-data about an association as it was specified in the
46
# ActiveModel::Serializer class.
57
#
68
# @example
7-
# class PostSerializer < ActiveModel::Serializer
9+
# class PostSerializer < ActiveModel::Serializer
810
# has_one :author, serializer: AuthorSerializer
911
# has_many :comments
1012
# has_many :comments, key: :last_comments do
1113
# object.comments.last(1)
1214
# end
13-
# end
15+
# has_many :secret_meta_data, if: :is_admin?
16+
#
17+
# def is_admin?
18+
# current_user.admin?
19+
# end
20+
# end
1421
#
15-
# Notice that the association block is evaluated in the context of the serializer.
1622
# Specifically, the association 'comments' is evaluated two different ways:
1723
# 1) as 'comments' and named 'comments'.
1824
# 2) as 'object.comments.last(1)' and named 'last_comments'.
@@ -21,20 +27,13 @@ class Serializer
2127
# # [
2228
# # HasOneReflection.new(:author, serializer: AuthorSerializer),
2329
# # HasManyReflection.new(:comments)
30+
# # HasManyReflection.new(:comments, { key: :last_comments }, #<Block>)
31+
# # HasManyReflection.new(:secret_meta_data, { if: :is_admin? })
2432
# # ]
2533
#
2634
# So you can inspect reflections in your Adapters.
2735
#
28-
Reflection = Struct.new(:name, :options, :block) do
29-
# @api private
30-
def value(instance)
31-
if block
32-
instance.instance_eval(&block)
33-
else
34-
instance.read_attribute_for_serialization(name)
35-
end
36-
end
37-
36+
class Reflection < Field
3837
# Build association. This method is used internally to
3938
# build serializer's association by its reflection.
4039
#

test/serializers/associations_test.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,29 @@ def test_associations_namespaced_resources
238238
end
239239
end
240240
end
241+
242+
def test_conditional_associations
243+
serializer = Class.new(ActiveModel::Serializer) do
244+
belongs_to :if_assoc_included, if: :true
245+
belongs_to :if_assoc_excluded, if: :false
246+
belongs_to :unless_assoc_included, unless: :false
247+
belongs_to :unless_assoc_excluded, unless: :true
248+
249+
def true
250+
true
251+
end
252+
253+
def false
254+
false
255+
end
256+
end
257+
258+
model = ::Model.new
259+
hash = serializable(model, serializer: serializer).serializable_hash
260+
expected = { if_assoc_included: nil, unless_assoc_included: nil }
261+
262+
assert_equal(expected, hash)
263+
end
241264
end
242265
end
243266
end

test/serializers/attribute_test.rb

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module ActiveModel
44
class Serializer
55
class AttributeTest < ActiveSupport::TestCase
66
def setup
7-
@blog = Blog.new({ id: 1, name: 'AMS Hints', type: 'stuff' })
7+
@blog = Blog.new(id: 1, name: 'AMS Hints', type: 'stuff')
88
@blog_serializer = AlternateBlogSerializer.new(@blog)
99
end
1010

@@ -95,6 +95,29 @@ def test_virtual_attribute_block
9595

9696
assert_equal(expected, hash)
9797
end
98+
99+
def test_conditional_attributes
100+
serializer = Class.new(ActiveModel::Serializer) do
101+
attribute :if_attribute_included, if: :true
102+
attribute :if_attribute_excluded, if: :false
103+
attribute :unless_attribute_included, unless: :false
104+
attribute :unless_attribute_excluded, unless: :true
105+
106+
def true
107+
true
108+
end
109+
110+
def false
111+
false
112+
end
113+
end
114+
115+
model = ::Model.new
116+
hash = serializable(model, serializer: serializer).serializable_hash
117+
expected = { if_attribute_included: nil, unless_attribute_included: nil }
118+
119+
assert_equal(expected, hash)
120+
end
98121
end
99122
end
100123
end

0 commit comments

Comments
 (0)