Skip to content

Commit ca76be1

Browse files
author
Noah Silas
committed
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.
1 parent 4bdf44a commit ca76be1

File tree

2 files changed

+42
-26
lines changed

2 files changed

+42
-26
lines changed

lib/active_model/serializer.rb

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,8 @@ def self.digest_caller_file(caller_line)
4646

4747
with_options instance_writer: false, instance_reader: false do |serializer|
4848
class_attribute :_type, instance_reader: true
49-
class_attribute :serialized_attributes, instance_writer: false # @api public: maps attribute name to 'Attribute' function
50-
self.serialized_attributes ||= {}
51-
class_attribute :_attributes_keys # @api private : maps attribute value to explict key name, @see Serializer#attribute
52-
self._attributes_keys ||= {}
49+
class_attribute :_attributes_map # @api private : maps attribute key names to names to names of implementing methods, @see Serializer#attribute
50+
self._attributes_map ||= {}
5351
class_attribute :_links # @api private : links definitions, @see Serializer#link
5452
self._links ||= {}
5553

@@ -73,8 +71,7 @@ def self.digest_caller_file(caller_line)
7371
# Generates a unique digest for each serializer at load.
7472
def self.inherited(base)
7573
caller_line = caller.first
76-
base.serialized_attributes = serialized_attributes.dup
77-
base._attributes_keys = _attributes_keys.dup
74+
base._attributes_map = _attributes_map.dup
7875
base._links = _links.dup
7976
base._cache_digest = digest_caller_file(caller_line)
8077
super
@@ -91,10 +88,6 @@ def self.link(name, value = nil, &block)
9188
_links[name] = block || value
9289
end
9390

94-
def self._attributes
95-
serialized_attributes.keys
96-
end
97-
9891
# @example
9992
# class AdminAuthorSerializer < ActiveModel::Serializer
10093
# attributes :id, :name, :recent_edits
@@ -121,21 +114,35 @@ def self.attributes(*attrs)
121114
# end
122115
def self.attribute(attr, options = {}, &block)
123116
key = options.fetch(:key, attr)
124-
_attributes_keys[attr] = { key: key } if key != attr
117+
reader = if block
118+
->(instance) { instance.instance_eval(&block) }
119+
else
120+
->(instance) { instance.send(attr) }
121+
end
125122

126-
if block_given?
127-
serialized_attributes[key] = ->(instance) { instance.instance_eval(&block) }
128-
else
129-
serialized_attributes[key] = ->(instance) { instance.object.read_attribute_for_serialization(attr) }
130-
end
123+
_attributes_map[key] = { attr: attr, reader: reader }
131124

132125
ActiveModelSerializers.silence_warnings do
133-
define_method key do
134-
serialized_attributes[key].call(self)
135-
end unless method_defined?(key) || _fragmented.respond_to?(attr)
126+
define_method attr do
127+
object.read_attribute_for_serialization(attr)
128+
end unless method_defined?(attr) || _fragmented.respond_to?(attr)
136129
end
137130
end
138131

132+
# @api private
133+
# An accessor for the old _attributes internal API
134+
def self._attributes
135+
_attributes_map.keys
136+
end
137+
138+
# @api private
139+
# An accessor for the old _attributes_keys internal API
140+
def self._attributes_keys
141+
_attributes_map
142+
.select { |key, details| key != details[:attr] }
143+
.each_with_object({}) { |(key, details), acc| acc[details[:attr]] = { key: key } }
144+
end
145+
139146
# @api private
140147
# Used by FragmentCache on the CachedSerializer
141148
# to call attribute methods on the fragmented cached serializer.
@@ -261,16 +268,16 @@ def json_key
261268
# Return the +attributes+ of +object+ as presented
262269
# by the serializer.
263270
def attributes(requested_attrs = nil)
264-
self.class._attributes.each_with_object({}) do |name, hash|
265-
next unless requested_attrs.nil? || requested_attrs.include?(name)
266-
hash[name] = read_attribute_for_serialization(name)
271+
self.class._attributes_map.each_with_object({}) do |(key, details), hash|
272+
next unless requested_attrs.nil? || requested_attrs.include?(key)
273+
if self.class._fragmented
274+
hash[key] = self.class._fragmented.public_send(details[:attr])
275+
else
276+
hash[key] = details[:reader].call(self)
277+
end
267278
end
268279
end
269280

270-
def read_attribute_for_serialization(key)
271-
self.class._fragmented ? self.class._fragmented.public_send(key) : send(key)
272-
end
273-
274281
# @api private
275282
# Used by JsonApi adapter to build resource links.
276283
def links

test/serializers/attribute_test.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ def test_id_attribute_override
4343
assert_equal({ blog: { id: 'AMS Hints' } }, adapter.serializable_hash)
4444
end
4545

46+
def test_object_attribute_override
47+
serializer = Class.new(ActiveModel::Serializer) do
48+
attribute :name, key: :object
49+
end
50+
51+
adapter = ActiveModel::Serializer::Adapter::Json.new(serializer.new(@blog))
52+
assert_equal({ blog: { object: 'AMS Hints' } }, adapter.serializable_hash)
53+
end
54+
4655
def test_type_attribute
4756
attribute_serializer = Class.new(ActiveModel::Serializer) do
4857
attribute :id, key: :type

0 commit comments

Comments
 (0)