From a502b5d38ba289b7aa14cbb35e143e464ca7f64f Mon Sep 17 00:00:00 2001 From: Lucas Hosseini Date: Wed, 30 Dec 2015 17:19:50 +0100 Subject: [PATCH 1/6] Add support for if/unless on attributes. --- lib/active_model/serializer/attribute.rb | 29 ++++++++++++++++++++++- lib/active_model/serializer/attributes.rb | 3 ++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/active_model/serializer/attribute.rb b/lib/active_model/serializer/attribute.rb index 5c9893ca..23d2b3d6 100644 --- a/lib/active_model/serializer/attribute.rb +++ b/lib/active_model/serializer/attribute.rb @@ -1,6 +1,6 @@ module ActiveModel class Serializer - Attribute = Struct.new(:name, :block) do + Attribute = Struct.new(:name, :options, :block) do def value(serializer) if block serializer.instance_eval(&block) @@ -8,6 +8,33 @@ module ActiveModel serializer.read_attribute_for_serialization(name) end end + + def included?(serializer) + case condition + when :if + serializer.public_send(condition) + when :unless + !serializer.public_send(condition) + else + true + end + end + + private + + def 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/attributes.rb b/lib/active_model/serializer/attributes.rb index f57ab205..6962f5ac 100644 --- a/lib/active_model/serializer/attributes.rb +++ b/lib/active_model/serializer/attributes.rb @@ -17,6 +17,7 @@ module ActiveModel def attributes(requested_attrs = nil, reload = false) @attributes = nil if reload @attributes ||= self.class._attributes_data.each_with_object({}) do |(key, attr), hash| + next unless attr.included?(self) next unless requested_attrs.nil? || requested_attrs.include?(key) hash[key] = attr.value(self) end @@ -54,7 +55,7 @@ module ActiveModel # 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 From 6860318133a1d5b1f61d4c51bc3c7d0b4a3f8205 Mon Sep 17 00:00:00 2001 From: Lucas Hosseini Date: Wed, 30 Dec 2015 17:25:41 +0100 Subject: [PATCH 2/6] Add support for if/unless on associations. --- CHANGELOG.md | 1 + lib/active_model/serializer/associations.rb | 1 + lib/active_model/serializer/reflection.rb | 26 +++++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f8bd834..a89c9e05 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 42e872ce..fe4bfe1f 100644 --- a/lib/active_model/serializer/associations.rb +++ b/lib/active_model/serializer/associations.rb @@ -88,6 +88,7 @@ module ActiveModel Enumerator.new do |y| self.class._reflections.each do |reflection| + next unless reflection.included?(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/reflection.rb b/lib/active_model/serializer/reflection.rb index 19eb78b8..484b95ae 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -35,6 +35,18 @@ module ActiveModel end end + # @api private + def included?(serializer) + case condition_type + when :if + serializer.public_send(condition) + when :unless + !serializer.public_send(condition) + else + true + end + end + # Build association. This method is used internally to # build serializer's association by its reflection. # @@ -79,6 +91,20 @@ module ActiveModel private + def condition_type + if options.key?(:if) + :if + elsif options.key?(:unless) + :unless + else + :none + end + end + + def condition + options[condition_type] + end + def serializer_options(subject, parent_serializer_options, reflection_options) serializer = reflection_options.fetch(:serializer, nil) From 40ed7b57bddf4950670e76c83628dfaf14f0d7ce Mon Sep 17 00:00:00 2001 From: Lucas Hosseini Date: Mon, 4 Jan 2016 20:40:56 +0100 Subject: [PATCH 3/6] Factor out ancestor class Field of Attribute and Reflection. --- lib/active_model/serializer/attribute.rb | 55 +++++++++-------------- lib/active_model/serializer/field.rb | 55 +++++++++++++++++++++++ lib/active_model/serializer/reflection.rb | 51 +++++---------------- 3 files changed, 87 insertions(+), 74 deletions(-) create mode 100644 lib/active_model/serializer/field.rb diff --git a/lib/active_model/serializer/attribute.rb b/lib/active_model/serializer/attribute.rb index 23d2b3d6..d3e006fa 100644 --- a/lib/active_model/serializer/attribute.rb +++ b/lib/active_model/serializer/attribute.rb @@ -1,40 +1,25 @@ +require 'active_model/serializer/field' + module ActiveModel class Serializer - Attribute = Struct.new(:name, :options, :block) do - def value(serializer) - if block - serializer.instance_eval(&block) - else - serializer.read_attribute_for_serialization(name) - end - end - - def included?(serializer) - case condition - when :if - serializer.public_send(condition) - when :unless - !serializer.public_send(condition) - else - true - end - end - - private - - def condition_type - if options.key?(:if) - :if - elsif options.key?(:unless) - :unless - else - :none - end - end - - def condition - options[condition_type] - 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/field.rb b/lib/active_model/serializer/field.rb new file mode 100644 index 00000000..79f97e7b --- /dev/null +++ b/lib/active_model/serializer/field.rb @@ -0,0 +1,55 @@ +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 included?(serializer) + case condition + when :if + serializer.public_send(condition) + when :unless + !serializer.public_send(condition) + else + true + end + end + + private + + def 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 484b95ae..c0287b64 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,32 +27,13 @@ module ActiveModel # # [ # # 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 - - # @api private - def included?(serializer) - case condition_type - when :if - serializer.public_send(condition) - when :unless - !serializer.public_send(condition) - else - true - end - end - + class Reflection < Field # Build association. This method is used internally to # build serializer's association by its reflection. # @@ -91,20 +78,6 @@ module ActiveModel private - def condition_type - if options.key?(:if) - :if - elsif options.key?(:unless) - :unless - else - :none - end - end - - def condition - options[condition_type] - end - def serializer_options(subject, parent_serializer_options, reflection_options) serializer = reflection_options.fetch(:serializer, nil) From 7fbf7e536dd223194933c32238bd9e72d39d3cc3 Mon Sep 17 00:00:00 2001 From: Chris Nixon Date: Sat, 9 Jan 2016 16:32:17 -0800 Subject: [PATCH 4/6] Use condition_type in case statement for included?. --- lib/active_model/serializer/field.rb | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/active_model/serializer/field.rb b/lib/active_model/serializer/field.rb index 79f97e7b..1134bb35 100644 --- a/lib/active_model/serializer/field.rb +++ b/lib/active_model/serializer/field.rb @@ -25,7 +25,7 @@ module ActiveModel # @api private # def included?(serializer) - case condition + case condition_type when :if serializer.public_send(condition) when :unless @@ -38,13 +38,14 @@ module ActiveModel private def condition_type - if options.key?(:if) - :if - elsif options.key?(:unless) - :unless - else - :none - end + @condition_type ||= + if options.key?(:if) + :if + elsif options.key?(:unless) + :unless + else + :none + end end def condition From 7af198653d052044c72012d10f9b0ae1a4c2f394 Mon Sep 17 00:00:00 2001 From: Lucas Hosseini Date: Tue, 12 Jan 2016 13:42:44 +0100 Subject: [PATCH 5/6] Add tests for conditional attributes/associations. --- test/serializers/associations_test.rb | 23 +++++++++++++++++++++++ test/serializers/attribute_test.rb | 25 ++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index 4778fb2e..aa0cae08 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -238,6 +238,29 @@ module ActiveModel 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 112e7ec5..c675e0ac 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 @@ module ActiveModel 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 From 2696557650b3fab5a6f203f9788d75deeae2ab30 Mon Sep 17 00:00:00 2001 From: Lucas Hosseini Date: Wed, 13 Jan 2016 05:13:20 +0100 Subject: [PATCH 6/6] Replace `Field#included?` with `Field#excluded?`. --- lib/active_model/serializer/associations.rb | 2 +- lib/active_model/serializer/attributes.rb | 2 +- lib/active_model/serializer/field.rb | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/active_model/serializer/associations.rb b/lib/active_model/serializer/associations.rb index fe4bfe1f..7d87156e 100644 --- a/lib/active_model/serializer/associations.rb +++ b/lib/active_model/serializer/associations.rb @@ -88,7 +88,7 @@ module ActiveModel Enumerator.new do |y| self.class._reflections.each do |reflection| - next unless reflection.included?(self) + 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/attributes.rb b/lib/active_model/serializer/attributes.rb index 6962f5ac..11d39c4b 100644 --- a/lib/active_model/serializer/attributes.rb +++ b/lib/active_model/serializer/attributes.rb @@ -17,7 +17,7 @@ module ActiveModel def attributes(requested_attrs = nil, reload = false) @attributes = nil if reload @attributes ||= self.class._attributes_data.each_with_object({}) do |(key, attr), hash| - next unless attr.included?(self) + next if attr.excluded?(self) next unless requested_attrs.nil? || requested_attrs.include?(key) hash[key] = attr.value(self) end diff --git a/lib/active_model/serializer/field.rb b/lib/active_model/serializer/field.rb index 1134bb35..35e6fe26 100644 --- a/lib/active_model/serializer/field.rb +++ b/lib/active_model/serializer/field.rb @@ -24,14 +24,14 @@ module ActiveModel # # @api private # - def included?(serializer) + def excluded?(serializer) case condition_type when :if - serializer.public_send(condition) - when :unless !serializer.public_send(condition) + when :unless + serializer.public_send(condition) else - true + false end end