diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f8bd8341..a89c9e051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/lib/active_model/serializer/associations.rb b/lib/active_model/serializer/associations.rb index 42e872ce4..7d87156e7 100644 --- a/lib/active_model/serializer/associations.rb +++ b/lib/active_model/serializer/associations.rb @@ -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) diff --git a/lib/active_model/serializer/attribute.rb b/lib/active_model/serializer/attribute.rb index 5c9893ca5..d3e006faa 100644 --- a/lib/active_model/serializer/attribute.rb +++ b/lib/active_model/serializer/attribute.rb @@ -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 diff --git a/lib/active_model/serializer/attributes.rb b/lib/active_model/serializer/attributes.rb index f57ab205d..11d39c4b2 100644 --- a/lib/active_model/serializer/attributes.rb +++ b/lib/active_model/serializer/attributes.rb @@ -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 @@ -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 diff --git a/lib/active_model/serializer/field.rb b/lib/active_model/serializer/field.rb new file mode 100644 index 000000000..35e6fe263 --- /dev/null +++ b/lib/active_model/serializer/field.rb @@ -0,0 +1,56 @@ +module ActiveModel + class Serializer + # Holds all the meta-data about a field (i.e. attribute or association) as it was + # 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 + # 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 diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index 19eb78b80..c0287b646 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -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 + # 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'. @@ -21,20 +27,13 @@ class Serializer # # [ # # HasOneReflection.new(:author, serializer: AuthorSerializer), # # HasManyReflection.new(:comments) + # # HasManyReflection.new(:comments, { key: :last_comments }, #) + # # 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. # diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index 4778fb2e2..aa0cae085 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -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 diff --git a/test/serializers/attribute_test.rb b/test/serializers/attribute_test.rb index 112e7ec51..c675e0aca 100644 --- a/test/serializers/attribute_test.rb +++ b/test/serializers/attribute_test.rb @@ -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 @@ -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) + end end end end