Skip to content

Commit

Permalink
Merge pull request #1699 from mtsmfm/str-lambda-support-for-if
Browse files Browse the repository at this point in the history
String/Lambda support for conditional attributes/associations
  • Loading branch information
bf4 committed Apr 26, 2016
2 parents d43b32a + aa087a2 commit 0433869
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 33 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Breaking changes:
- [#1662](https://github.com/rails-api/active_model_serializers/pull/1662) Drop support for Rails 4.0 and Ruby 2.0.0. (@remear)

Features:
- [#1699](https://github.com/rails-api/active_model_serializers/pull/1699) String/Lambda support for conditional attributes/associations (@mtsmfm)
- [#1687](https://github.com/rails-api/active_model_serializers/pull/1687) Only calculate `_cache_digest` (in `cache_key`) when `skip_digest` is false. (@bf4)
- [#1647](https://github.com/rails-api/active_model_serializers/pull/1647) Restrict usage of `serializable_hash` options
to the ActiveModel::Serialization and ActiveModel::Serializers::JSON interface. (@bf4)
Expand Down
4 changes: 4 additions & 0 deletions docs/general/serializers.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ end

```ruby
has_one :blog, if: :show_blog?
# you can also use a string or lambda
# has_one :blog, if: 'scope.admin?'
# has_one :blog, if: -> (serializer) { serializer.scope.admin? }
# has_one :blog, if: -> { scope.admin? }

def show_blog?
scope.admin?
Expand Down
38 changes: 36 additions & 2 deletions lib/active_model/serializer/field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ class Serializer
# 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
def initialize(*)
super

validate_condition!
end

# 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
Expand All @@ -27,16 +33,44 @@ def value(serializer)
def excluded?(serializer)
case condition_type
when :if
!serializer.public_send(condition)
!evaluate_condition(serializer)
when :unless
serializer.public_send(condition)
evaluate_condition(serializer)
else
false
end
end

private

def validate_condition!
return if condition_type == :none

case condition
when Symbol, String, Proc
# noop
else
fail TypeError, "#{condition_type.inspect} should be a Symbol, String or Proc"
end
end

def evaluate_condition(serializer)
case condition
when Symbol
serializer.public_send(condition)
when String
serializer.instance_eval(condition)
when Proc
if condition.arity.zero?
serializer.instance_exec(&condition)
else
serializer.instance_exec(serializer, &condition)
end
else
nil
end
end

def condition_type
@condition_type ||=
if options.key?(:if)
Expand Down
58 changes: 43 additions & 15 deletions test/serializers/associations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -239,27 +239,55 @@ def test_associations_namespaced_resources
end
end

# rubocop:disable Metrics/AbcSize
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
model = ::Model.new(true: true, false: false)

scenarios = [
{ options: { if: :true }, included: true },
{ options: { if: :false }, included: false },
{ options: { unless: :false }, included: true },
{ options: { unless: :true }, included: false },
{ options: { if: 'object.true' }, included: true },
{ options: { if: 'object.false' }, included: false },
{ options: { unless: 'object.false' }, included: true },
{ options: { unless: 'object.true' }, included: false },
{ options: { if: -> { object.true } }, included: true },
{ options: { if: -> { object.false } }, included: false },
{ options: { unless: -> { object.false } }, included: true },
{ options: { unless: -> { object.true } }, included: false },
{ options: { if: -> (s) { s.object.true } }, included: true },
{ options: { if: -> (s) { s.object.false } }, included: false },
{ options: { unless: -> (s) { s.object.false } }, included: true },
{ options: { unless: -> (s) { s.object.true } }, included: false }
]

scenarios.each do |s|
serializer = Class.new(ActiveModel::Serializer) do
belongs_to :association, s[:options]

def true
true
end

def false
false
def false
false
end
end

hash = serializable(model, serializer: serializer).serializable_hash
assert_equal(s[:included], hash.key?(:association), "Error with #{s[:options]}")
end
end

model = ::Model.new
hash = serializable(model, serializer: serializer).serializable_hash
expected = { if_assoc_included: nil, unless_assoc_included: nil }
def test_illegal_conditional_associations
exception = assert_raises(TypeError) do
Class.new(ActiveModel::Serializer) do
belongs_to :x, if: nil
end
end

assert_equal(expected, hash)
assert_match(/:if should be a Symbol, String or Proc/, exception.message)
end
end
end
Expand Down
60 changes: 44 additions & 16 deletions test/serializers/attribute_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,27 +96,55 @@ 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
# rubocop:disable Metrics/AbcSize
def test_conditional_associations
model = ::Model.new(true: true, false: false)

scenarios = [
{ options: { if: :true }, included: true },
{ options: { if: :false }, included: false },
{ options: { unless: :false }, included: true },
{ options: { unless: :true }, included: false },
{ options: { if: 'object.true' }, included: true },
{ options: { if: 'object.false' }, included: false },
{ options: { unless: 'object.false' }, included: true },
{ options: { unless: 'object.true' }, included: false },
{ options: { if: -> { object.true } }, included: true },
{ options: { if: -> { object.false } }, included: false },
{ options: { unless: -> { object.false } }, included: true },
{ options: { unless: -> { object.true } }, included: false },
{ options: { if: -> (s) { s.object.true } }, included: true },
{ options: { if: -> (s) { s.object.false } }, included: false },
{ options: { unless: -> (s) { s.object.false } }, included: true },
{ options: { unless: -> (s) { s.object.true } }, included: false }
]

scenarios.each do |s|
serializer = Class.new(ActiveModel::Serializer) do
attribute :attribute, s[:options]

def true
true
end

def false
false
end
end

def false
false
end
hash = serializable(model, serializer: serializer).serializable_hash
assert_equal(s[:included], hash.key?(:attribute), "Error with #{s[:options]}")
end
end

model = ::Model.new
hash = serializable(model, serializer: serializer).serializable_hash
expected = { if_attribute_included: nil, unless_attribute_included: nil }
def test_illegal_conditional_attributes
exception = assert_raises(TypeError) do
Class.new(ActiveModel::Serializer) do
attribute :x, if: nil
end
end

assert_equal(expected, hash)
assert_match(/:if should be a Symbol, String or Proc/, exception.message)
end
end
end
Expand Down

0 comments on commit 0433869

Please sign in to comment.