Skip to content

Commit 0433869

Browse files
committed
Merge pull request #1699 from mtsmfm/str-lambda-support-for-if
String/Lambda support for conditional attributes/associations
2 parents d43b32a + aa087a2 commit 0433869

File tree

5 files changed

+128
-33
lines changed

5 files changed

+128
-33
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Breaking changes:
66
- [#1662](https://github.com/rails-api/active_model_serializers/pull/1662) Drop support for Rails 4.0 and Ruby 2.0.0. (@remear)
77

88
Features:
9+
- [#1699](https://github.com/rails-api/active_model_serializers/pull/1699) String/Lambda support for conditional attributes/associations (@mtsmfm)
910
- [#1687](https://github.com/rails-api/active_model_serializers/pull/1687) Only calculate `_cache_digest` (in `cache_key`) when `skip_digest` is false. (@bf4)
1011
- [#1647](https://github.com/rails-api/active_model_serializers/pull/1647) Restrict usage of `serializable_hash` options
1112
to the ActiveModel::Serialization and ActiveModel::Serializers::JSON interface. (@bf4)

docs/general/serializers.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ end
8080

8181
```ruby
8282
has_one :blog, if: :show_blog?
83+
# you can also use a string or lambda
84+
# has_one :blog, if: 'scope.admin?'
85+
# has_one :blog, if: -> (serializer) { serializer.scope.admin? }
86+
# has_one :blog, if: -> { scope.admin? }
8387

8488
def show_blog?
8589
scope.admin?

lib/active_model/serializer/field.rb

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ class Serializer
44
# specified in the ActiveModel::Serializer class.
55
# Notice that the field block is evaluated in the context of the serializer.
66
Field = Struct.new(:name, :options, :block) do
7+
def initialize(*)
8+
super
9+
10+
validate_condition!
11+
end
12+
713
# Compute the actual value of a field for a given serializer instance.
814
# @param [Serializer] The serializer instance for which the value is computed.
915
# @return [Object] value
@@ -27,16 +33,44 @@ def value(serializer)
2733
def excluded?(serializer)
2834
case condition_type
2935
when :if
30-
!serializer.public_send(condition)
36+
!evaluate_condition(serializer)
3137
when :unless
32-
serializer.public_send(condition)
38+
evaluate_condition(serializer)
3339
else
3440
false
3541
end
3642
end
3743

3844
private
3945

46+
def validate_condition!
47+
return if condition_type == :none
48+
49+
case condition
50+
when Symbol, String, Proc
51+
# noop
52+
else
53+
fail TypeError, "#{condition_type.inspect} should be a Symbol, String or Proc"
54+
end
55+
end
56+
57+
def evaluate_condition(serializer)
58+
case condition
59+
when Symbol
60+
serializer.public_send(condition)
61+
when String
62+
serializer.instance_eval(condition)
63+
when Proc
64+
if condition.arity.zero?
65+
serializer.instance_exec(&condition)
66+
else
67+
serializer.instance_exec(serializer, &condition)
68+
end
69+
else
70+
nil
71+
end
72+
end
73+
4074
def condition_type
4175
@condition_type ||=
4276
if options.key?(:if)

test/serializers/associations_test.rb

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -239,27 +239,55 @@ def test_associations_namespaced_resources
239239
end
240240
end
241241

242+
# rubocop:disable Metrics/AbcSize
242243
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
244+
model = ::Model.new(true: true, false: false)
245+
246+
scenarios = [
247+
{ options: { if: :true }, included: true },
248+
{ options: { if: :false }, included: false },
249+
{ options: { unless: :false }, included: true },
250+
{ options: { unless: :true }, included: false },
251+
{ options: { if: 'object.true' }, included: true },
252+
{ options: { if: 'object.false' }, included: false },
253+
{ options: { unless: 'object.false' }, included: true },
254+
{ options: { unless: 'object.true' }, included: false },
255+
{ options: { if: -> { object.true } }, included: true },
256+
{ options: { if: -> { object.false } }, included: false },
257+
{ options: { unless: -> { object.false } }, included: true },
258+
{ options: { unless: -> { object.true } }, included: false },
259+
{ options: { if: -> (s) { s.object.true } }, included: true },
260+
{ options: { if: -> (s) { s.object.false } }, included: false },
261+
{ options: { unless: -> (s) { s.object.false } }, included: true },
262+
{ options: { unless: -> (s) { s.object.true } }, included: false }
263+
]
264+
265+
scenarios.each do |s|
266+
serializer = Class.new(ActiveModel::Serializer) do
267+
belongs_to :association, s[:options]
268+
269+
def true
270+
true
271+
end
252272

253-
def false
254-
false
273+
def false
274+
false
275+
end
255276
end
277+
278+
hash = serializable(model, serializer: serializer).serializable_hash
279+
assert_equal(s[:included], hash.key?(:association), "Error with #{s[:options]}")
256280
end
281+
end
257282

258-
model = ::Model.new
259-
hash = serializable(model, serializer: serializer).serializable_hash
260-
expected = { if_assoc_included: nil, unless_assoc_included: nil }
283+
def test_illegal_conditional_associations
284+
exception = assert_raises(TypeError) do
285+
Class.new(ActiveModel::Serializer) do
286+
belongs_to :x, if: nil
287+
end
288+
end
261289

262-
assert_equal(expected, hash)
290+
assert_match(/:if should be a Symbol, String or Proc/, exception.message)
263291
end
264292
end
265293
end

test/serializers/attribute_test.rb

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -96,27 +96,55 @@ def test_virtual_attribute_block
9696
assert_equal(expected, hash)
9797
end
9898

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
99+
# rubocop:disable Metrics/AbcSize
100+
def test_conditional_associations
101+
model = ::Model.new(true: true, false: false)
102+
103+
scenarios = [
104+
{ options: { if: :true }, included: true },
105+
{ options: { if: :false }, included: false },
106+
{ options: { unless: :false }, included: true },
107+
{ options: { unless: :true }, included: false },
108+
{ options: { if: 'object.true' }, included: true },
109+
{ options: { if: 'object.false' }, included: false },
110+
{ options: { unless: 'object.false' }, included: true },
111+
{ options: { unless: 'object.true' }, included: false },
112+
{ options: { if: -> { object.true } }, included: true },
113+
{ options: { if: -> { object.false } }, included: false },
114+
{ options: { unless: -> { object.false } }, included: true },
115+
{ options: { unless: -> { object.true } }, included: false },
116+
{ options: { if: -> (s) { s.object.true } }, included: true },
117+
{ options: { if: -> (s) { s.object.false } }, included: false },
118+
{ options: { unless: -> (s) { s.object.false } }, included: true },
119+
{ options: { unless: -> (s) { s.object.true } }, included: false }
120+
]
121+
122+
scenarios.each do |s|
123+
serializer = Class.new(ActiveModel::Serializer) do
124+
attribute :attribute, s[:options]
125+
126+
def true
127+
true
128+
end
129+
130+
def false
131+
false
132+
end
108133
end
109134

110-
def false
111-
false
112-
end
135+
hash = serializable(model, serializer: serializer).serializable_hash
136+
assert_equal(s[:included], hash.key?(:attribute), "Error with #{s[:options]}")
113137
end
138+
end
114139

115-
model = ::Model.new
116-
hash = serializable(model, serializer: serializer).serializable_hash
117-
expected = { if_attribute_included: nil, unless_attribute_included: nil }
140+
def test_illegal_conditional_attributes
141+
exception = assert_raises(TypeError) do
142+
Class.new(ActiveModel::Serializer) do
143+
attribute :x, if: nil
144+
end
145+
end
118146

119-
assert_equal(expected, hash)
147+
assert_match(/:if should be a Symbol, String or Proc/, exception.message)
120148
end
121149
end
122150
end

0 commit comments

Comments
 (0)