From 87d18e9c32981d417afa103db2e2a821832fa9fb Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Mon, 30 Nov 2015 15:20:14 -0600 Subject: [PATCH 01/14] Map attributes to Attribute values when defined in serializer --- lib/active_model/serializer.rb | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index a34136c9f..f7e7b435e 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -46,8 +46,8 @@ def self.digest_caller_file(caller_line) with_options instance_writer: false, instance_reader: false do |serializer| class_attribute :_type, instance_reader: true - class_attribute :_attributes # @api private : names of attribute methods, @see Serializer#attribute - self._attributes ||= [] + class_attribute :serialized_attributes, instance_writer: false # @api public: maps attribute name to 'Attribute' function + self.serialized_attributes ||= {} class_attribute :_attributes_keys # @api private : maps attribute value to explict key name, @see Serializer#attribute self._attributes_keys ||= {} class_attribute :_links # @api private : links definitions, @see Serializer#link @@ -69,11 +69,11 @@ def self.digest_caller_file(caller_line) serializer.class_attribute :_cache_digest # @api private : Generated end - # Serializers inherit _attributes and _attributes_keys. + # Serializers inherit serialized_attributes and _attributes_keys. # Generates a unique digest for each serializer at load. def self.inherited(base) caller_line = caller.first - base._attributes = _attributes.dup + base.serialized_attributes = serialized_attributes.dup base._attributes_keys = _attributes_keys.dup base._links = _links.dup base._cache_digest = digest_caller_file(caller_line) @@ -91,6 +91,10 @@ def self.link(name, value = nil, &block) _links[name] = block || value end + def self._attributes + serialized_attributes.keys + end + # @example # class AdminAuthorSerializer < ActiveModel::Serializer # attributes :id, :name, :recent_edits @@ -102,6 +106,7 @@ def self.attributes(*attrs) end end + # TODO: remove the dynamic method definition # @example # class AdminAuthorSerializer < ActiveModel::Serializer # attributes :id, :recent_edits @@ -109,15 +114,17 @@ def self.attributes(*attrs) # # def recent_edits # object.edits.last(5) - # enr + # end def self.attribute(attr, options = {}) key = options.fetch(:key, attr) _attributes_keys[attr] = { key: key } if key != attr _attributes << key unless _attributes.include?(key) + serialized_attributes[key] = ->(object) { object.read_attribute_for_serialization(attr) } + ActiveModelSerializers.silence_warnings do define_method key do - object.read_attribute_for_serialization(attr) + serialized_attributes[key].call(object) end unless method_defined?(key) || _fragmented.respond_to?(attr) end end @@ -249,14 +256,14 @@ def json_key def attributes(requested_attrs = nil) self.class._attributes.each_with_object({}) do |name, hash| next unless requested_attrs.nil? || requested_attrs.include?(name) - if self.class._fragmented - hash[name] = self.class._fragmented.public_send(name) - else - hash[name] = send(name) - end + hash[name] = read_attribute_for_serialization(name) end end + def read_attribute_for_serialization(key) + self.class._fragmented ? self.class._fragmented.public_send(key) : send(key) + end + # @api private # Used by JsonApi adapter to build resource links. def links From 6020450fe4e1a3a947de36e233bdfeccafe81a33 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Mon, 30 Nov 2015 16:45:17 -0600 Subject: [PATCH 02/14] Allow specifying attributes with a block Adapted from https://github.com/rails-api/active_model_serializers/pull/1262 --- lib/active_model/serializer.rb | 15 +++++++++++---- test/serializers/attribute_test.rb | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index f7e7b435e..25d508481 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -112,19 +112,26 @@ def self.attributes(*attrs) # attributes :id, :recent_edits # attribute :name, key: :title # + # attribute :full_name do + # "#{object.first_name} #{object.last_name}" + # end + # # def recent_edits # object.edits.last(5) # end - def self.attribute(attr, options = {}) + def self.attribute(attr, options = {}, &block) key = options.fetch(:key, attr) _attributes_keys[attr] = { key: key } if key != attr - _attributes << key unless _attributes.include?(key) - serialized_attributes[key] = ->(object) { object.read_attribute_for_serialization(attr) } + if block_given? + serialized_attributes[key] = ->(instance) { instance.instance_eval(&block) } + else + serialized_attributes[key] = ->(instance) { instance.object.read_attribute_for_serialization(attr) } + end ActiveModelSerializers.silence_warnings do define_method key do - serialized_attributes[key].call(object) + serialized_attributes[key].call(self) end unless method_defined?(key) || _fragmented.respond_to?(attr) end end diff --git a/test/serializers/attribute_test.rb b/test/serializers/attribute_test.rb index 99452e530..e1368c27e 100644 --- a/test/serializers/attribute_test.rb +++ b/test/serializers/attribute_test.rb @@ -71,6 +71,21 @@ def id assert_equal('custom', hash[:blog][:id]) end + + PostWithVirtualAttribute = Class.new(::Model) + class PostWithVirtualAttributeSerializer < ActiveModel::Serializer + attribute :name do + "#{object.first_name} #{object.last_name}" + end + end + + def test_virtual_attribute_block + post = PostWithVirtualAttribute.new(first_name: 'Lucas', last_name: 'Hosseini') + hash = serializable(post).serializable_hash + expected = { name: 'Lucas Hosseini' } + + assert_equal(expected, hash) + end end end end From 7cbef1b3b594cf1d2a9d7d2bc8d0afa38cd3693c Mon Sep 17 00:00:00 2001 From: Lucas Hosseini Date: Tue, 20 Oct 2015 00:54:56 +0200 Subject: [PATCH 03/14] Add inline syntax for defining associations Adapted from https://github.com/rails-api/active_model_serializers/pull/1262 --- lib/active_model/serializer.rb | 2 +- lib/active_model/serializer/associations.rb | 29 ++++++++++++--------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 25d508481..75b8c10c8 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -69,7 +69,7 @@ def self.digest_caller_file(caller_line) serializer.class_attribute :_cache_digest # @api private : Generated end - # Serializers inherit serialized_attributes and _attributes_keys. + # Serializers inherit serialized_attributes, _attributes_keys, and _reflections. # Generates a unique digest for each serializer at load. def self.inherited(base) caller_line = caller.first diff --git a/lib/active_model/serializer/associations.rb b/lib/active_model/serializer/associations.rb index af627a13c..a07b52ca5 100644 --- a/lib/active_model/serializer/associations.rb +++ b/lib/active_model/serializer/associations.rb @@ -13,9 +13,8 @@ module Associations DEFAULT_INCLUDE_TREE = ActiveModel::Serializer::IncludeTree.from_string('*') included do |base| - class << base - attr_accessor :_reflections - end + base.class_attribute :_reflections + base._reflections ||= [] extend ActiveSupport::Autoload autoload :Association @@ -28,8 +27,10 @@ class << base end module ClassMethods + # Serializers inherit _reflections. def inherited(base) - base._reflections = self._reflections.try(:dup) || [] + super + base._reflections = _reflections.dup end # @param [Symbol] name of the association @@ -39,8 +40,8 @@ def inherited(base) # @example # has_many :comments, serializer: CommentSummarySerializer # - def has_many(name, options = {}) - associate HasManyReflection.new(name, options) + def has_many(name, options = {}, &block) + associate(HasManyReflection.new(name, options), block) end # @param [Symbol] name of the association @@ -50,8 +51,8 @@ def has_many(name, options = {}) # @example # belongs_to :author, serializer: AuthorSerializer # - def belongs_to(name, options = {}) - associate BelongsToReflection.new(name, options) + def belongs_to(name, options = {}, &block) + associate(BelongsToReflection.new(name, options), block) end # @param [Symbol] name of the association @@ -61,8 +62,8 @@ def belongs_to(name, options = {}) # @example # has_one :author, serializer: AuthorSerializer # - def has_one(name, options = {}) - associate HasOneReflection.new(name, options) + def has_one(name, options = {}, &block) + associate(HasOneReflection.new(name, options), block) end private @@ -73,11 +74,15 @@ def has_one(name, options = {}) # # @api private # - def associate(reflection) + def associate(reflection, block) self._reflections = _reflections.dup define_method reflection.name do - object.send reflection.name + if block_given? + instance_eval(&block) + else + object.send reflection.name + end end unless method_defined?(reflection.name) self._reflections << reflection From e2903643c5232f11f55de63f4d4c46f83861777c Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Mon, 30 Nov 2015 17:47:12 -0600 Subject: [PATCH 04/14] Encapsulate serialized_associations; test inline associations --- lib/active_model/serializer/associations.rb | 21 +++++++++------ test/serializers/associations_test.rb | 29 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/lib/active_model/serializer/associations.rb b/lib/active_model/serializer/associations.rb index a07b52ca5..0842b9c33 100644 --- a/lib/active_model/serializer/associations.rb +++ b/lib/active_model/serializer/associations.rb @@ -13,7 +13,9 @@ module Associations DEFAULT_INCLUDE_TREE = ActiveModel::Serializer::IncludeTree.from_string('*') included do |base| - base.class_attribute :_reflections + base.class_attribute :serialized_associations, instance_writer: false # @api public: maps association name to 'Reflection' instance + base.serialized_associations ||= {} + base.class_attribute :_reflections, instance_writer: false base._reflections ||= [] extend ActiveSupport::Autoload @@ -77,13 +79,16 @@ def has_one(name, options = {}, &block) def associate(reflection, block) self._reflections = _reflections.dup - define_method reflection.name do - if block_given? - instance_eval(&block) - else - object.send reflection.name - end - end unless method_defined?(reflection.name) + reflection_name = reflection.name + if block + serialized_associations[reflection_name] = ->(instance) { instance.instance_eval(&block) } + else + serialized_associations[reflection_name] = ->(instance) { instance.object.send(reflection_name) } + end + + define_method reflection_name do + serialized_associations[reflection_name].call(self) + end unless method_defined?(reflection_name) self._reflections << reflection end diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index 205cfcc88..81380c7c7 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -126,6 +126,35 @@ def test_associations_custom_keys assert expected_association_keys.include? :site end + class InlineAssociationTestPostSerializer < ActiveModel::Serializer + has_many :comments + has_many :last_comments do + object.comments.last(1) + end + end + + def test_virtual_attribute_block + comment1 = ::ARModels::Comment.create!(contents: 'first comment') + comment2 = ::ARModels::Comment.create!(contents: 'last comment') + post = ::ARModels::Post.create!( + title: 'inline association test', + body: 'etc', + comments: [comment1, comment2] + ) + actual = serializable(post, adapter: :attributes, serializer: InlineAssociationTestPostSerializer).as_json + expected = { + :comments => [ + { :id => 1, :contents => 'first comment' }, + { :id => 2, :contents => 'last comment' } + ], + :last_comments => [ + { :id => 2, :contents => 'last comment' } + ] + } + + assert_equal expected, actual + end + class NamespacedResourcesTest < Minitest::Test class ResourceNamespace Post = Class.new(::Model) From 7bde7bf752091db741bcdc4cc83154615171d5a8 Mon Sep 17 00:00:00 2001 From: Noah Silas Date: Wed, 25 Nov 2015 18:46:00 +0000 Subject: [PATCH 05/14] Handle conflicts between key names and serializer methods As an example, all serializers implement `#object` as a reference to the object being esrialized, but this was preventing adding a key to the serialized representation with the `object` name. Instead of having attributes directly map to methods on the serializer, we introduce one layer of abstraction: the `_attributes_map`. This hash maps the key names expected in the output to the names of the implementing methods. This simplifies some things (removing the need to maintain both `_attributes` and `_attribute_keys`), but does add some complexity in order to support overriding attributes by defining methods on the serializer. It seems that with the addition of the inline-block format, we may want to remove the usage of programatically defining methods on the serializer for this kind of customization. --- lib/active_model/serializer.rb | 59 +++++++++++++++++------------- test/serializers/attribute_test.rb | 9 +++++ 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 75b8c10c8..390ce2ecb 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -46,10 +46,8 @@ def self.digest_caller_file(caller_line) with_options instance_writer: false, instance_reader: false do |serializer| class_attribute :_type, instance_reader: true - class_attribute :serialized_attributes, instance_writer: false # @api public: maps attribute name to 'Attribute' function - self.serialized_attributes ||= {} - class_attribute :_attributes_keys # @api private : maps attribute value to explict key name, @see Serializer#attribute - self._attributes_keys ||= {} + class_attribute :_attributes_map # @api private : maps attribute key names to names to names of implementing methods, @see Serializer#attribute + self._attributes_map ||= {} class_attribute :_links # @api private : links definitions, @see Serializer#link self._links ||= {} @@ -73,8 +71,7 @@ def self.digest_caller_file(caller_line) # Generates a unique digest for each serializer at load. def self.inherited(base) caller_line = caller.first - base.serialized_attributes = serialized_attributes.dup - base._attributes_keys = _attributes_keys.dup + base._attributes_map = _attributes_map.dup base._links = _links.dup base._cache_digest = digest_caller_file(caller_line) super @@ -91,10 +88,6 @@ def self.link(name, value = nil, &block) _links[name] = block || value end - def self._attributes - serialized_attributes.keys - end - # @example # class AdminAuthorSerializer < ActiveModel::Serializer # attributes :id, :name, :recent_edits @@ -121,21 +114,35 @@ def self.attributes(*attrs) # end def self.attribute(attr, options = {}, &block) key = options.fetch(:key, attr) - _attributes_keys[attr] = { key: key } if key != attr + reader = if block + ->(instance) { instance.instance_eval(&block) } + else + ->(instance) { instance.send(attr) } + end - if block_given? - serialized_attributes[key] = ->(instance) { instance.instance_eval(&block) } - else - serialized_attributes[key] = ->(instance) { instance.object.read_attribute_for_serialization(attr) } - end + _attributes_map[key] = { attr: attr, reader: reader } ActiveModelSerializers.silence_warnings do - define_method key do - serialized_attributes[key].call(self) - end unless method_defined?(key) || _fragmented.respond_to?(attr) + define_method attr do + object.read_attribute_for_serialization(attr) + end unless method_defined?(attr) || _fragmented.respond_to?(attr) end end + # @api private + # An accessor for the old _attributes internal API + def self._attributes + _attributes_map.keys + end + + # @api private + # An accessor for the old _attributes_keys internal API + def self._attributes_keys + _attributes_map + .select { |key, details| key != details[:attr] } + .each_with_object({}) { |(key, details), acc| acc[details[:attr]] = { key: key } } + end + # @api private # Used by FragmentCache on the CachedSerializer # to call attribute methods on the fragmented cached serializer. @@ -261,16 +268,16 @@ def json_key # Return the +attributes+ of +object+ as presented # by the serializer. def attributes(requested_attrs = nil) - self.class._attributes.each_with_object({}) do |name, hash| - next unless requested_attrs.nil? || requested_attrs.include?(name) - hash[name] = read_attribute_for_serialization(name) + self.class._attributes_map.each_with_object({}) do |(key, details), hash| + next unless requested_attrs.nil? || requested_attrs.include?(key) + if self.class._fragmented + hash[key] = self.class._fragmented.public_send(details[:attr]) + else + hash[key] = details[:reader].call(self) + end end end - def read_attribute_for_serialization(key) - self.class._fragmented ? self.class._fragmented.public_send(key) : send(key) - end - # @api private # Used by JsonApi adapter to build resource links. def links diff --git a/test/serializers/attribute_test.rb b/test/serializers/attribute_test.rb index e1368c27e..cf9dae4f1 100644 --- a/test/serializers/attribute_test.rb +++ b/test/serializers/attribute_test.rb @@ -43,6 +43,15 @@ def test_id_attribute_override assert_equal({ blog: { id: 'AMS Hints' } }, adapter.serializable_hash) end + def test_object_attribute_override + serializer = Class.new(ActiveModel::Serializer) do + attribute :name, key: :object + end + + adapter = ActiveModel::Serializer::Adapter::Json.new(serializer.new(@blog)) + assert_equal({ blog: { object: 'AMS Hints' } }, adapter.serializable_hash) + end + def test_type_attribute attribute_serializer = Class.new(ActiveModel::Serializer) do attribute :id, key: :type From 0bf45ec2a7c87cf087252686c57ebf53b401ac24 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Tue, 1 Dec 2015 23:39:40 -0600 Subject: [PATCH 06/14] Small refactor to Serializer::_attribute_mappings --- lib/active_model/serializer.rb | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 390ce2ecb..a76a928f3 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -14,6 +14,9 @@ class Serializer include Configuration include Associations require 'active_model/serializer/adapter' + Attribute = Struct.new(:name, :reader) do + delegate :call, to: :reader + end # Matches # "c:/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb:1:in `'" @@ -46,8 +49,8 @@ def self.digest_caller_file(caller_line) with_options instance_writer: false, instance_reader: false do |serializer| class_attribute :_type, instance_reader: true - class_attribute :_attributes_map # @api private : maps attribute key names to names to names of implementing methods, @see Serializer#attribute - self._attributes_map ||= {} + class_attribute :_attribute_mappings # @api private : maps attribute key names to names to names of implementing methods, @see Serializer#attribute + self._attribute_mappings ||= {} class_attribute :_links # @api private : links definitions, @see Serializer#link self._links ||= {} @@ -71,7 +74,7 @@ def self.digest_caller_file(caller_line) # Generates a unique digest for each serializer at load. def self.inherited(base) caller_line = caller.first - base._attributes_map = _attributes_map.dup + base._attribute_mappings = _attribute_mappings.dup base._links = _links.dup base._cache_digest = digest_caller_file(caller_line) super @@ -120,7 +123,7 @@ def self.attribute(attr, options = {}, &block) ->(instance) { instance.send(attr) } end - _attributes_map[key] = { attr: attr, reader: reader } + _attribute_mappings[key] = Attribute.new(attr, reader) ActiveModelSerializers.silence_warnings do define_method attr do @@ -130,17 +133,22 @@ def self.attribute(attr, options = {}, &block) end # @api private - # An accessor for the old _attributes internal API + # names of attribute methods + # @see Serializer::attribute def self._attributes - _attributes_map.keys + _attribute_mappings.keys end # @api private - # An accessor for the old _attributes_keys internal API + # maps attribute value to explict key name + # @see Serializer::attribute + # @see Adapter::FragmentCache#fragment_serializer def self._attributes_keys - _attributes_map - .select { |key, details| key != details[:attr] } - .each_with_object({}) { |(key, details), acc| acc[details[:attr]] = { key: key } } + _attribute_mappings + .each_with_object({}) do |(key, attribute_mapping), hash| + next if key == attribute_mapping.name + hash[attribute_mapping.name] = { key: key } + end end # @api private @@ -268,12 +276,12 @@ def json_key # Return the +attributes+ of +object+ as presented # by the serializer. def attributes(requested_attrs = nil) - self.class._attributes_map.each_with_object({}) do |(key, details), hash| + self.class._attribute_mappings.each_with_object({}) do |(key, attribute_mapping), hash| next unless requested_attrs.nil? || requested_attrs.include?(key) if self.class._fragmented - hash[key] = self.class._fragmented.public_send(details[:attr]) + hash[key] = self.class._fragmented.public_send(attribute_mapping.name) else - hash[key] = details[:reader].call(self) + hash[key] = attribute_mapping.call(self) end end end From 8804d758efbf6610105bd31b7cae4ef000713760 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Wed, 2 Dec 2015 00:17:00 -0600 Subject: [PATCH 07/14] Remove dynamically defined instance methods --- lib/active_model/serializer.rb | 34 ++++++++++++++++++++++------------ test/fixtures/poro.rb | 2 +- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index a76a928f3..f933f024e 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -119,17 +119,13 @@ def self.attribute(attr, options = {}, &block) key = options.fetch(:key, attr) reader = if block ->(instance) { instance.instance_eval(&block) } + elsif _fragmented + ->(instance) { instance.class._fragmented.read_attribute_for_serialization(attr) } else - ->(instance) { instance.send(attr) } + ->(instance) { instance.read_attribute_for_serialization(attr) } end _attribute_mappings[key] = Attribute.new(attr, reader) - - ActiveModelSerializers.silence_warnings do - define_method attr do - object.read_attribute_for_serialization(attr) - end unless method_defined?(attr) || _fragmented.respond_to?(attr) - end end # @api private @@ -278,11 +274,15 @@ def json_key def attributes(requested_attrs = nil) self.class._attribute_mappings.each_with_object({}) do |(key, attribute_mapping), hash| next unless requested_attrs.nil? || requested_attrs.include?(key) - if self.class._fragmented - hash[key] = self.class._fragmented.public_send(attribute_mapping.name) - else - hash[key] = attribute_mapping.call(self) - end + hash[key] = attribute_mapping.call(self) + end + end + + def read_attribute_for_serialization(attr) + if _serializer_method_defined?(attr) + send(attr) + else + object.read_attribute_for_serialization(attr) end end @@ -295,5 +295,15 @@ def links protected attr_accessor :instance_options + + private + + def _serializer_instance_methods + @_serializer_instance_methods ||= (public_methods - Object.public_instance_methods).to_set + end + + def _serializer_method_defined?(name) + _serializer_instance_methods.include?(name) + end end end diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb index 573b3fa8d..5a6e3681e 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -116,7 +116,7 @@ def custom_options attributes :id, :name, :description, :slug def slug - "#{name}-#{id}" + "#{object.name}-#{object.id}" end belongs_to :author From eceb2d5598f93176e9e9bdb13590af37cc6bcac4 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Wed, 2 Dec 2015 17:33:57 -0600 Subject: [PATCH 08/14] Refactor serializer attribute objects --- lib/active_model/serializer.rb | 39 +++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index f933f024e..d6d096f05 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -14,8 +14,33 @@ class Serializer include Configuration include Associations require 'active_model/serializer/adapter' - Attribute = Struct.new(:name, :reader) do + class Attribute delegate :call, to: :reader + attr_reader :name, :reader + def initialize(name) + @name = name + @reader = nil + end + + def self.build(name, block) + if block + AttributeBlock.new(name, block) + else + AttributeReader.new(name) + end + end + end + class AttributeReader < Attribute + def initialize(name) + super(name) + @reader = ->(instance) { instance.read_attribute_for_serialization(name) } + end + end + class AttributeBlock < Attribute + def initialize(name, block) + super(name) + @reader = ->(instance) { instance.instance_eval(&block) } + end end # Matches @@ -117,15 +142,7 @@ def self.attributes(*attrs) # end def self.attribute(attr, options = {}, &block) key = options.fetch(:key, attr) - reader = if block - ->(instance) { instance.instance_eval(&block) } - elsif _fragmented - ->(instance) { instance.class._fragmented.read_attribute_for_serialization(attr) } - else - ->(instance) { instance.read_attribute_for_serialization(attr) } - end - - _attribute_mappings[key] = Attribute.new(attr, reader) + _attribute_mappings[key] = Attribute.build(attr, block) end # @api private @@ -281,6 +298,8 @@ def attributes(requested_attrs = nil) def read_attribute_for_serialization(attr) if _serializer_method_defined?(attr) send(attr) + elsif self.class._fragmented + self.class._fragmented.read_attribute_for_serialization(attr) else object.read_attribute_for_serialization(attr) end From 036604b1494f1634701684d9b409061da1539e25 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Wed, 2 Dec 2015 17:45:33 -0600 Subject: [PATCH 09/14] Extract Serializer Attributes into its own file --- lib/active_model/serializer.rb | 97 +------------------- lib/active_model/serializer/attributes.rb | 107 ++++++++++++++++++++++ 2 files changed, 112 insertions(+), 92 deletions(-) create mode 100644 lib/active_model/serializer/attributes.rb diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index d6d096f05..030e7c83a 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -3,6 +3,7 @@ require 'active_model/serializer/array_serializer' require 'active_model/serializer/include_tree' require 'active_model/serializer/associations' +require 'active_model/serializer/attributes' require 'active_model/serializer/configuration' require 'active_model/serializer/fieldset' require 'active_model/serializer/lint' @@ -13,35 +14,8 @@ module ActiveModel class Serializer include Configuration include Associations + include Attributes require 'active_model/serializer/adapter' - class Attribute - delegate :call, to: :reader - attr_reader :name, :reader - def initialize(name) - @name = name - @reader = nil - end - - def self.build(name, block) - if block - AttributeBlock.new(name, block) - else - AttributeReader.new(name) - end - end - end - class AttributeReader < Attribute - def initialize(name) - super(name) - @reader = ->(instance) { instance.read_attribute_for_serialization(name) } - end - end - class AttributeBlock < Attribute - def initialize(name, block) - super(name) - @reader = ->(instance) { instance.instance_eval(&block) } - end - end # Matches # "c:/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb:1:in `'" @@ -73,12 +47,9 @@ def self.digest_caller_file(caller_line) end with_options instance_writer: false, instance_reader: false do |serializer| - class_attribute :_type, instance_reader: true - class_attribute :_attribute_mappings # @api private : maps attribute key names to names to names of implementing methods, @see Serializer#attribute - self._attribute_mappings ||= {} - class_attribute :_links # @api private : links definitions, @see Serializer#link + serializer.class_attribute :_type, instance_reader: true + serializer.class_attribute :_links # @api private : links definitions, @see Serializer#link self._links ||= {} - serializer.class_attribute :_cache # @api private : the cache object serializer.class_attribute :_fragmented # @api private : @see ::fragmented serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key @@ -95,11 +66,10 @@ def self.digest_caller_file(caller_line) serializer.class_attribute :_cache_digest # @api private : Generated end - # Serializers inherit serialized_attributes, _attributes_keys, and _reflections. + # Serializers inherit _attribute_mappings, _reflections, and _links. # Generates a unique digest for each serializer at load. def self.inherited(base) caller_line = caller.first - base._attribute_mappings = _attribute_mappings.dup base._links = _links.dup base._cache_digest = digest_caller_file(caller_line) super @@ -116,54 +86,6 @@ def self.link(name, value = nil, &block) _links[name] = block || value end - # @example - # class AdminAuthorSerializer < ActiveModel::Serializer - # attributes :id, :name, :recent_edits - def self.attributes(*attrs) - attrs = attrs.first if attrs.first.class == Array - - attrs.each do |attr| - attribute(attr) - end - end - - # TODO: remove the dynamic method definition - # @example - # class AdminAuthorSerializer < ActiveModel::Serializer - # attributes :id, :recent_edits - # attribute :name, key: :title - # - # attribute :full_name do - # "#{object.first_name} #{object.last_name}" - # end - # - # def recent_edits - # object.edits.last(5) - # end - def self.attribute(attr, options = {}, &block) - key = options.fetch(:key, attr) - _attribute_mappings[key] = Attribute.build(attr, block) - end - - # @api private - # names of attribute methods - # @see Serializer::attribute - def self._attributes - _attribute_mappings.keys - end - - # @api private - # maps attribute value to explict key name - # @see Serializer::attribute - # @see Adapter::FragmentCache#fragment_serializer - def self._attributes_keys - _attribute_mappings - .each_with_object({}) do |(key, attribute_mapping), hash| - next if key == attribute_mapping.name - hash[attribute_mapping.name] = { key: key } - end - end - # @api private # Used by FragmentCache on the CachedSerializer # to call attribute methods on the fragmented cached serializer. @@ -286,15 +208,6 @@ def json_key root || object.class.model_name.to_s.underscore end - # Return the +attributes+ of +object+ as presented - # by the serializer. - def attributes(requested_attrs = nil) - self.class._attribute_mappings.each_with_object({}) do |(key, attribute_mapping), hash| - next unless requested_attrs.nil? || requested_attrs.include?(key) - hash[key] = attribute_mapping.call(self) - end - end - def read_attribute_for_serialization(attr) if _serializer_method_defined?(attr) send(attr) diff --git a/lib/active_model/serializer/attributes.rb b/lib/active_model/serializer/attributes.rb new file mode 100644 index 000000000..1c271f01a --- /dev/null +++ b/lib/active_model/serializer/attributes.rb @@ -0,0 +1,107 @@ +module ActiveModel + class Serializer + module Attributes + class Attribute + delegate :call, to: :reader + attr_reader :name, :reader + def initialize(name) + @name = name + @reader = nil + end + + def self.build(name, block) + if block + AttributeBlock.new(name, block) + else + AttributeReader.new(name) + end + end + end + class AttributeReader < Attribute + def initialize(name) + super(name) + @reader = ->(instance) { instance.read_attribute_for_serialization(name) } + end + end + class AttributeBlock < Attribute + def initialize(name, block) + super(name) + @reader = ->(instance) { instance.instance_eval(&block) } + end + end + + extend ActiveSupport::Concern + + included do + with_options instance_writer: false, instance_reader: false do |serializer| + serializer.class_attribute :_attribute_mappings # @api private : maps attribute key names to names to names of implementing methods, @see #attribute + self._attribute_mappings ||= {} + end + + # Return the +attributes+ of +object+ as presented + # by the serializer. + def attributes(requested_attrs = nil) + self.class._attribute_mappings.each_with_object({}) do |(key, attribute_mapping), hash| + next unless requested_attrs.nil? || requested_attrs.include?(key) + hash[key] = attribute_mapping.call(self) + end + end + end + + module ClassMethods + def inherited(base) + super + base._attribute_mappings = _attribute_mappings.dup + end + + # @example + # class AdminAuthorSerializer < ActiveModel::Serializer + # attributes :id, :name, :recent_edits + def attributes(*attrs) + attrs = attrs.first if attrs.first.class == Array + + attrs.each do |attr| + attribute(attr) + end + end + + # TODO: remove the dynamic method definition + # @example + # class AdminAuthorSerializer < ActiveModel::Serializer + # attributes :id, :recent_edits + # attribute :name, key: :title + # + # attribute :full_name do + # "#{object.first_name} #{object.last_name}" + # end + # + # def recent_edits + # object.edits.last(5) + # end + def attribute(attr, options = {}, &block) + key = options.fetch(:key, attr) + _attribute_mappings[key] = Attribute.build(attr, block) + end + + # @api private + # names of attribute methods + # @see Serializer::attribute + def _attributes + _attribute_mappings.keys + end + + # @api private + # maps attribute value to explict key name + # @see Serializer::attribute + # @see Adapter::FragmentCache#fragment_serializer + def _attributes_keys + _attribute_mappings + .each_with_object({}) do |(key, attribute_mapping), hash| + next if key == attribute_mapping.name + hash[attribute_mapping.name] = { key: key } + end + end + end + end + end +end From cd736e0adf133f9b3a50de422f4e61a1c4dbbff1 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Wed, 2 Dec 2015 17:47:24 -0600 Subject: [PATCH 10/14] Memoize attributes --- lib/active_model/serializer/attributes.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/active_model/serializer/attributes.rb b/lib/active_model/serializer/attributes.rb index 1c271f01a..bea270e79 100644 --- a/lib/active_model/serializer/attributes.rb +++ b/lib/active_model/serializer/attributes.rb @@ -40,8 +40,9 @@ def initialize(name, block) # Return the +attributes+ of +object+ as presented # by the serializer. - def attributes(requested_attrs = nil) - self.class._attribute_mappings.each_with_object({}) do |(key, attribute_mapping), hash| + def attributes(requested_attrs = nil, reload = false) + @attributes = nil if reload + @attributes ||= self.class._attribute_mappings.each_with_object({}) do |(key, attribute_mapping), hash| next unless requested_attrs.nil? || requested_attrs.include?(key) hash[key] = attribute_mapping.call(self) end @@ -65,7 +66,6 @@ def attributes(*attrs) end end - # TODO: remove the dynamic method definition # @example # class AdminAuthorSerializer < ActiveModel::Serializer # attributes :id, :recent_edits From c4feccfd10b536dd6ec01eebf1645e574f70b55f Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Thu, 3 Dec 2015 10:53:43 -0600 Subject: [PATCH 11/14] Refactor Association/Reflection block value reading --- lib/active_model/serializer/associations.rb | 30 +++++++-------------- lib/active_model/serializer/attributes.rb | 4 ++- lib/active_model/serializer/reflection.rb | 25 +++++++++++++++-- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/lib/active_model/serializer/associations.rb b/lib/active_model/serializer/associations.rb index 0842b9c33..c4da3515d 100644 --- a/lib/active_model/serializer/associations.rb +++ b/lib/active_model/serializer/associations.rb @@ -12,11 +12,11 @@ module Associations DEFAULT_INCLUDE_TREE = ActiveModel::Serializer::IncludeTree.from_string('*') - included do |base| - base.class_attribute :serialized_associations, instance_writer: false # @api public: maps association name to 'Reflection' instance - base.serialized_associations ||= {} - base.class_attribute :_reflections, instance_writer: false - base._reflections ||= [] + included do + with_options instance_writer: false, instance_reader: true do |serializer| + serializer.class_attribute :_reflections + self._reflections ||= [] + end extend ActiveSupport::Autoload autoload :Association @@ -29,7 +29,6 @@ module Associations end module ClassMethods - # Serializers inherit _reflections. def inherited(base) super base._reflections = _reflections.dup @@ -43,7 +42,7 @@ def inherited(base) # has_many :comments, serializer: CommentSummarySerializer # def has_many(name, options = {}, &block) - associate(HasManyReflection.new(name, options), block) + associate(HasManyReflection.new(name, options, block)) end # @param [Symbol] name of the association @@ -54,7 +53,7 @@ def has_many(name, options = {}, &block) # belongs_to :author, serializer: AuthorSerializer # def belongs_to(name, options = {}, &block) - associate(BelongsToReflection.new(name, options), block) + associate(BelongsToReflection.new(name, options, block)) end # @param [Symbol] name of the association @@ -65,7 +64,7 @@ def belongs_to(name, options = {}, &block) # has_one :author, serializer: AuthorSerializer # def has_one(name, options = {}, &block) - associate(HasOneReflection.new(name, options), block) + associate(HasOneReflection.new(name, options, block)) end private @@ -76,20 +75,9 @@ def has_one(name, options = {}, &block) # # @api private # - def associate(reflection, block) + def associate(reflection) self._reflections = _reflections.dup - reflection_name = reflection.name - if block - serialized_associations[reflection_name] = ->(instance) { instance.instance_eval(&block) } - else - serialized_associations[reflection_name] = ->(instance) { instance.object.send(reflection_name) } - end - - define_method reflection_name do - serialized_associations[reflection_name].call(self) - end unless method_defined?(reflection_name) - self._reflections << reflection end end diff --git a/lib/active_model/serializer/attributes.rb b/lib/active_model/serializer/attributes.rb index bea270e79..f46b0f481 100644 --- a/lib/active_model/serializer/attributes.rb +++ b/lib/active_model/serializer/attributes.rb @@ -3,10 +3,12 @@ class Serializer module Attributes class Attribute delegate :call, to: :reader + attr_reader :name, :reader + def initialize(name) @name = name - @reader = nil + @reader = :no_reader end def self.build(name, block) diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index 18850abe3..e2d16d359 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -17,7 +17,28 @@ class Serializer # # So you can inspect reflections in your Adapters. # - Reflection = Struct.new(:name, :options) do + Reflection = Struct.new(:name, :options, :block) do + delegate :call, to: :reader + + attr_reader :reader + + def initialize(*) + super + @reader = self.class.build_reader(name, block) + end + + def value(instance) + call(instance) + end + + def self.build_reader(name, block) + if block + ->(instance) { instance.instance_eval(&block) } + else + ->(instance) { instance.read_attribute_for_serialization(name) } + end + end + # Build association. This method is used internally to # build serializer's association by its reflection. # @@ -40,7 +61,7 @@ class Serializer # @api private # def build_association(subject, parent_serializer_options) - association_value = subject.send(name) + association_value = value(subject) reflection_options = options.dup serializer_class = subject.class.serializer_for(association_value, reflection_options) From 3e8290a9237a95a31a49cffda995842a0dca3afb Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Fri, 4 Dec 2015 13:31:29 -0600 Subject: [PATCH 12/14] Serializer instance methods don't change; track at class level Per groyoh https://github.com/rails-api/active_model_serializers/pull/1356#discussion_r46713503 --- lib/active_model/serializer.rb | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 030e7c83a..eadcb32e8 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -184,6 +184,15 @@ def self.get_serializer_for(klass) end end + def self._serializer_instance_method_defined?(name) + _serializer_instance_methods.include?(name) + end + + def self._serializer_instance_methods + @_serializer_instance_methods ||= (public_instance_methods - Object.public_instance_methods).to_set + end + private_class_method :_serializer_instance_methods + attr_accessor :object, :root, :scope # `scope_name` is set as :current_user by default in the controller. @@ -209,7 +218,7 @@ def json_key end def read_attribute_for_serialization(attr) - if _serializer_method_defined?(attr) + if self.class._serializer_instance_method_defined?(attr) send(attr) elsif self.class._fragmented self.class._fragmented.read_attribute_for_serialization(attr) @@ -227,15 +236,5 @@ def links protected attr_accessor :instance_options - - private - - def _serializer_instance_methods - @_serializer_instance_methods ||= (public_methods - Object.public_instance_methods).to_set - end - - def _serializer_method_defined?(name) - _serializer_instance_methods.include?(name) - end end end From 386a567dfc5229d6123720b652dbc54ea0a9a5be Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Fri, 4 Dec 2015 13:58:22 -0600 Subject: [PATCH 13/14] Evaluate association blocks as scopes on the association --- lib/active_model/serializer/reflection.rb | 10 +++++++++- test/serializers/associations_test.rb | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index e2d16d359..472b2991d 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -7,8 +7,16 @@ class Serializer # class PostSerializer < ActiveModel::Serializer # has_one :author, serializer: AuthorSerializer # has_many :comments + # has_many :comments, key: :last_comments do + # last(1) + # end # end # + # Notice that the association block is evaluated in the context of the association. + # Specifically, the association 'comments' is evaluated two different ways: + # 1) as 'comments' and named 'comments'. + # 2) as 'comments.last(1)' and named 'last_comments'. + # # PostSerializer._reflections #=> # # [ # # HasOneReflection.new(:author, serializer: AuthorSerializer), @@ -33,7 +41,7 @@ def value(instance) def self.build_reader(name, block) if block - ->(instance) { instance.instance_eval(&block) } + ->(instance) { instance.read_attribute_for_serialization(name).instance_eval(&block) } else ->(instance) { instance.read_attribute_for_serialization(name) } end diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index 81380c7c7..ecb671f2a 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -128,8 +128,8 @@ def test_associations_custom_keys class InlineAssociationTestPostSerializer < ActiveModel::Serializer has_many :comments - has_many :last_comments do - object.comments.last(1) + has_many :comments, key: :last_comments do + last(1) end end From bf8270b8b4c5eda6c4da1ad801d52a293736cced Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Thu, 10 Dec 2015 15:08:22 -0600 Subject: [PATCH 14/14] Document Serializer settings and private api [ci skip] --- lib/active_model/serializer/attributes.rb | 3 +++ lib/active_model/serializer/reflection.rb | 2 ++ 2 files changed, 5 insertions(+) diff --git a/lib/active_model/serializer/attributes.rb b/lib/active_model/serializer/attributes.rb index f46b0f481..81f6e49af 100644 --- a/lib/active_model/serializer/attributes.rb +++ b/lib/active_model/serializer/attributes.rb @@ -1,6 +1,7 @@ module ActiveModel class Serializer module Attributes + # @api private class Attribute delegate :call, to: :reader @@ -19,12 +20,14 @@ def self.build(name, block) end end end + # @api private class AttributeReader < Attribute def initialize(name) super(name) @reader = ->(instance) { instance.read_attribute_for_serialization(name) } end end + # @api private class AttributeBlock < Attribute def initialize(name, block) super(name) diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index 472b2991d..c027d96e0 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -35,10 +35,12 @@ def initialize(*) @reader = self.class.build_reader(name, block) end + # @api private def value(instance) call(instance) end + # @api private def self.build_reader(name, block) if block ->(instance) { instance.read_attribute_for_serialization(name).instance_eval(&block) }