mirror of
https://github.com/ditkrg/active_model_serializers.git
synced 2026-01-22 22:06:50 +00:00
String/Lambda support for conditional attributes/associations
This commit is contained in:
parent
d43b32a4d3
commit
aa087a22b5
@ -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)
|
- [#1662](https://github.com/rails-api/active_model_serializers/pull/1662) Drop support for Rails 4.0 and Ruby 2.0.0. (@remear)
|
||||||
|
|
||||||
Features:
|
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)
|
- [#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
|
- [#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)
|
to the ActiveModel::Serialization and ActiveModel::Serializers::JSON interface. (@bf4)
|
||||||
|
|||||||
@ -80,6 +80,10 @@ end
|
|||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
has_one :blog, if: :show_blog?
|
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?
|
def show_blog?
|
||||||
scope.admin?
|
scope.admin?
|
||||||
|
|||||||
@ -4,6 +4,12 @@ module ActiveModel
|
|||||||
# specified in the ActiveModel::Serializer class.
|
# specified in the ActiveModel::Serializer class.
|
||||||
# Notice that the field block is evaluated in the context of the serializer.
|
# Notice that the field block is evaluated in the context of the serializer.
|
||||||
Field = Struct.new(:name, :options, :block) do
|
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.
|
# Compute the actual value of a field for a given serializer instance.
|
||||||
# @param [Serializer] The serializer instance for which the value is computed.
|
# @param [Serializer] The serializer instance for which the value is computed.
|
||||||
# @return [Object] value
|
# @return [Object] value
|
||||||
@ -27,9 +33,9 @@ module ActiveModel
|
|||||||
def excluded?(serializer)
|
def excluded?(serializer)
|
||||||
case condition_type
|
case condition_type
|
||||||
when :if
|
when :if
|
||||||
!serializer.public_send(condition)
|
!evaluate_condition(serializer)
|
||||||
when :unless
|
when :unless
|
||||||
serializer.public_send(condition)
|
evaluate_condition(serializer)
|
||||||
else
|
else
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
@ -37,6 +43,34 @@ module ActiveModel
|
|||||||
|
|
||||||
private
|
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
|
def condition_type
|
||||||
@condition_type ||=
|
@condition_type ||=
|
||||||
if options.key?(:if)
|
if options.key?(:if)
|
||||||
|
|||||||
@ -239,27 +239,55 @@ module ActiveModel
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# rubocop:disable Metrics/AbcSize
|
||||||
def test_conditional_associations
|
def test_conditional_associations
|
||||||
serializer = Class.new(ActiveModel::Serializer) do
|
model = ::Model.new(true: true, false: false)
|
||||||
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
|
scenarios = [
|
||||||
true
|
{ 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
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def false
|
hash = serializable(model, serializer: serializer).serializable_hash
|
||||||
false
|
assert_equal(s[:included], hash.key?(:association), "Error with #{s[:options]}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_illegal_conditional_associations
|
||||||
|
exception = assert_raises(TypeError) do
|
||||||
|
Class.new(ActiveModel::Serializer) do
|
||||||
|
belongs_to :x, if: nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
model = ::Model.new
|
assert_match(/:if should be a Symbol, String or Proc/, exception.message)
|
||||||
hash = serializable(model, serializer: serializer).serializable_hash
|
|
||||||
expected = { if_assoc_included: nil, unless_assoc_included: nil }
|
|
||||||
|
|
||||||
assert_equal(expected, hash)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -96,27 +96,55 @@ module ActiveModel
|
|||||||
assert_equal(expected, hash)
|
assert_equal(expected, hash)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_conditional_attributes
|
# rubocop:disable Metrics/AbcSize
|
||||||
serializer = Class.new(ActiveModel::Serializer) do
|
def test_conditional_associations
|
||||||
attribute :if_attribute_included, if: :true
|
model = ::Model.new(true: true, false: false)
|
||||||
attribute :if_attribute_excluded, if: :false
|
|
||||||
attribute :unless_attribute_included, unless: :false
|
|
||||||
attribute :unless_attribute_excluded, unless: :true
|
|
||||||
|
|
||||||
def true
|
scenarios = [
|
||||||
true
|
{ 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
|
end
|
||||||
|
|
||||||
def false
|
hash = serializable(model, serializer: serializer).serializable_hash
|
||||||
false
|
assert_equal(s[:included], hash.key?(:attribute), "Error with #{s[:options]}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_illegal_conditional_attributes
|
||||||
|
exception = assert_raises(TypeError) do
|
||||||
|
Class.new(ActiveModel::Serializer) do
|
||||||
|
attribute :x, if: nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
model = ::Model.new
|
assert_match(/:if should be a Symbol, String or Proc/, exception.message)
|
||||||
hash = serializable(model, serializer: serializer).serializable_hash
|
|
||||||
expected = { if_attribute_included: nil, unless_attribute_included: nil }
|
|
||||||
|
|
||||||
assert_equal(expected, hash)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user