Skip to content

Commit f562449

Browse files
committed
Merge pull request #1356 from bf4/attribute_objects
Add inline syntax for attributes and associations
2 parents 614e349 + bf8270b commit f562449

File tree

7 files changed

+232
-68
lines changed

7 files changed

+232
-68
lines changed

lib/active_model/serializer.rb

Lines changed: 21 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'active_model/serializer/array_serializer'
44
require 'active_model/serializer/include_tree'
55
require 'active_model/serializer/associations'
6+
require 'active_model/serializer/attributes'
67
require 'active_model/serializer/configuration'
78
require 'active_model/serializer/fieldset'
89
require 'active_model/serializer/lint'
@@ -13,6 +14,7 @@ module ActiveModel
1314
class Serializer
1415
include Configuration
1516
include Associations
17+
include Attributes
1618
require 'active_model/serializer/adapter'
1719

1820
# Matches
@@ -45,14 +47,9 @@ def self.digest_caller_file(caller_line)
4547
end
4648

4749
with_options instance_writer: false, instance_reader: false do |serializer|
48-
class_attribute :_type, instance_reader: true
49-
class_attribute :_attributes # @api private : names of attribute methods, @see Serializer#attribute
50-
self._attributes ||= []
51-
class_attribute :_attributes_keys # @api private : maps attribute value to explict key name, @see Serializer#attribute
52-
self._attributes_keys ||= {}
53-
class_attribute :_links # @api private : links definitions, @see Serializer#link
50+
serializer.class_attribute :_type, instance_reader: true
51+
serializer.class_attribute :_links # @api private : links definitions, @see Serializer#link
5452
self._links ||= {}
55-
5653
serializer.class_attribute :_cache # @api private : the cache object
5754
serializer.class_attribute :_fragmented # @api private : @see ::fragmented
5855
serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key
@@ -69,12 +66,10 @@ def self.digest_caller_file(caller_line)
6966
serializer.class_attribute :_cache_digest # @api private : Generated
7067
end
7168

72-
# Serializers inherit _attributes and _attributes_keys.
69+
# Serializers inherit _attribute_mappings, _reflections, and _links.
7370
# Generates a unique digest for each serializer at load.
7471
def self.inherited(base)
7572
caller_line = caller.first
76-
base._attributes = _attributes.dup
77-
base._attributes_keys = _attributes_keys.dup
7873
base._links = _links.dup
7974
base._cache_digest = digest_caller_file(caller_line)
8075
super
@@ -91,37 +86,6 @@ def self.link(name, value = nil, &block)
9186
_links[name] = block || value
9287
end
9388

94-
# @example
95-
# class AdminAuthorSerializer < ActiveModel::Serializer
96-
# attributes :id, :name, :recent_edits
97-
def self.attributes(*attrs)
98-
attrs = attrs.first if attrs.first.class == Array
99-
100-
attrs.each do |attr|
101-
attribute(attr)
102-
end
103-
end
104-
105-
# @example
106-
# class AdminAuthorSerializer < ActiveModel::Serializer
107-
# attributes :id, :recent_edits
108-
# attribute :name, key: :title
109-
#
110-
# def recent_edits
111-
# object.edits.last(5)
112-
# enr
113-
def self.attribute(attr, options = {})
114-
key = options.fetch(:key, attr)
115-
_attributes_keys[attr] = { key: key } if key != attr
116-
_attributes << key unless _attributes.include?(key)
117-
118-
ActiveModelSerializers.silence_warnings do
119-
define_method key do
120-
object.read_attribute_for_serialization(attr)
121-
end unless method_defined?(key) || _fragmented.respond_to?(attr)
122-
end
123-
end
124-
12589
# @api private
12690
# Used by FragmentCache on the CachedSerializer
12791
# to call attribute methods on the fragmented cached serializer.
@@ -220,6 +184,15 @@ def self.get_serializer_for(klass)
220184
end
221185
end
222186

187+
def self._serializer_instance_method_defined?(name)
188+
_serializer_instance_methods.include?(name)
189+
end
190+
191+
def self._serializer_instance_methods
192+
@_serializer_instance_methods ||= (public_instance_methods - Object.public_instance_methods).to_set
193+
end
194+
private_class_method :_serializer_instance_methods
195+
223196
attr_accessor :object, :root, :scope
224197

225198
# `scope_name` is set as :current_user by default in the controller.
@@ -244,16 +217,13 @@ def json_key
244217
root || object.class.model_name.to_s.underscore
245218
end
246219

247-
# Return the +attributes+ of +object+ as presented
248-
# by the serializer.
249-
def attributes(requested_attrs = nil)
250-
self.class._attributes.each_with_object({}) do |name, hash|
251-
next unless requested_attrs.nil? || requested_attrs.include?(name)
252-
if self.class._fragmented
253-
hash[name] = self.class._fragmented.public_send(name)
254-
else
255-
hash[name] = send(name)
256-
end
220+
def read_attribute_for_serialization(attr)
221+
if self.class._serializer_instance_method_defined?(attr)
222+
send(attr)
223+
elsif self.class._fragmented
224+
self.class._fragmented.read_attribute_for_serialization(attr)
225+
else
226+
object.read_attribute_for_serialization(attr)
257227
end
258228
end
259229

lib/active_model/serializer/associations.rb

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ module Associations
1212

1313
DEFAULT_INCLUDE_TREE = ActiveModel::Serializer::IncludeTree.from_string('*')
1414

15-
included do |base|
16-
class << base
17-
attr_accessor :_reflections
15+
included do
16+
with_options instance_writer: false, instance_reader: true do |serializer|
17+
serializer.class_attribute :_reflections
18+
self._reflections ||= []
1819
end
1920

2021
extend ActiveSupport::Autoload
@@ -29,7 +30,8 @@ class << base
2930

3031
module ClassMethods
3132
def inherited(base)
32-
base._reflections = self._reflections.try(:dup) || []
33+
super
34+
base._reflections = _reflections.dup
3335
end
3436

3537
# @param [Symbol] name of the association
@@ -39,8 +41,8 @@ def inherited(base)
3941
# @example
4042
# has_many :comments, serializer: CommentSummarySerializer
4143
#
42-
def has_many(name, options = {})
43-
associate HasManyReflection.new(name, options)
44+
def has_many(name, options = {}, &block)
45+
associate(HasManyReflection.new(name, options, block))
4446
end
4547

4648
# @param [Symbol] name of the association
@@ -50,8 +52,8 @@ def has_many(name, options = {})
5052
# @example
5153
# belongs_to :author, serializer: AuthorSerializer
5254
#
53-
def belongs_to(name, options = {})
54-
associate BelongsToReflection.new(name, options)
55+
def belongs_to(name, options = {}, &block)
56+
associate(BelongsToReflection.new(name, options, block))
5557
end
5658

5759
# @param [Symbol] name of the association
@@ -61,8 +63,8 @@ def belongs_to(name, options = {})
6163
# @example
6264
# has_one :author, serializer: AuthorSerializer
6365
#
64-
def has_one(name, options = {})
65-
associate HasOneReflection.new(name, options)
66+
def has_one(name, options = {}, &block)
67+
associate(HasOneReflection.new(name, options, block))
6668
end
6769

6870
private
@@ -76,10 +78,6 @@ def has_one(name, options = {})
7678
def associate(reflection)
7779
self._reflections = _reflections.dup
7880

79-
define_method reflection.name do
80-
object.send reflection.name
81-
end unless method_defined?(reflection.name)
82-
8381
self._reflections << reflection
8482
end
8583
end
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
module ActiveModel
2+
class Serializer
3+
module Attributes
4+
# @api private
5+
class Attribute
6+
delegate :call, to: :reader
7+
8+
attr_reader :name, :reader
9+
10+
def initialize(name)
11+
@name = name
12+
@reader = :no_reader
13+
end
14+
15+
def self.build(name, block)
16+
if block
17+
AttributeBlock.new(name, block)
18+
else
19+
AttributeReader.new(name)
20+
end
21+
end
22+
end
23+
# @api private
24+
class AttributeReader < Attribute
25+
def initialize(name)
26+
super(name)
27+
@reader = ->(instance) { instance.read_attribute_for_serialization(name) }
28+
end
29+
end
30+
# @api private
31+
class AttributeBlock < Attribute
32+
def initialize(name, block)
33+
super(name)
34+
@reader = ->(instance) { instance.instance_eval(&block) }
35+
end
36+
end
37+
38+
extend ActiveSupport::Concern
39+
40+
included do
41+
with_options instance_writer: false, instance_reader: false do |serializer|
42+
serializer.class_attribute :_attribute_mappings # @api private : maps attribute key names to names to names of implementing methods, @see #attribute
43+
self._attribute_mappings ||= {}
44+
end
45+
46+
# Return the +attributes+ of +object+ as presented
47+
# by the serializer.
48+
def attributes(requested_attrs = nil, reload = false)
49+
@attributes = nil if reload
50+
@attributes ||= self.class._attribute_mappings.each_with_object({}) do |(key, attribute_mapping), hash|
51+
next unless requested_attrs.nil? || requested_attrs.include?(key)
52+
hash[key] = attribute_mapping.call(self)
53+
end
54+
end
55+
end
56+
57+
module ClassMethods
58+
def inherited(base)
59+
super
60+
base._attribute_mappings = _attribute_mappings.dup
61+
end
62+
63+
# @example
64+
# class AdminAuthorSerializer < ActiveModel::Serializer
65+
# attributes :id, :name, :recent_edits
66+
def attributes(*attrs)
67+
attrs = attrs.first if attrs.first.class == Array
68+
69+
attrs.each do |attr|
70+
attribute(attr)
71+
end
72+
end
73+
74+
# @example
75+
# class AdminAuthorSerializer < ActiveModel::Serializer
76+
# attributes :id, :recent_edits
77+
# attribute :name, key: :title
78+
#
79+
# attribute :full_name do
80+
# "#{object.first_name} #{object.last_name}"
81+
# end
82+
#
83+
# def recent_edits
84+
# object.edits.last(5)
85+
# end
86+
def attribute(attr, options = {}, &block)
87+
key = options.fetch(:key, attr)
88+
_attribute_mappings[key] = Attribute.build(attr, block)
89+
end
90+
91+
# @api private
92+
# names of attribute methods
93+
# @see Serializer::attribute
94+
def _attributes
95+
_attribute_mappings.keys
96+
end
97+
98+
# @api private
99+
# maps attribute value to explict key name
100+
# @see Serializer::attribute
101+
# @see Adapter::FragmentCache#fragment_serializer
102+
def _attributes_keys
103+
_attribute_mappings
104+
.each_with_object({}) do |(key, attribute_mapping), hash|
105+
next if key == attribute_mapping.name
106+
hash[attribute_mapping.name] = { key: key }
107+
end
108+
end
109+
end
110+
end
111+
end
112+
end

lib/active_model/serializer/reflection.rb

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@ class Serializer
77
# class PostSerializer < ActiveModel::Serializer
88
# has_one :author, serializer: AuthorSerializer
99
# has_many :comments
10+
# has_many :comments, key: :last_comments do
11+
# last(1)
12+
# end
1013
# end
1114
#
15+
# Notice that the association block is evaluated in the context of the association.
16+
# Specifically, the association 'comments' is evaluated two different ways:
17+
# 1) as 'comments' and named 'comments'.
18+
# 2) as 'comments.last(1)' and named 'last_comments'.
19+
#
1220
# PostSerializer._reflections #=>
1321
# # [
1422
# # HasOneReflection.new(:author, serializer: AuthorSerializer),
@@ -17,7 +25,30 @@ class Serializer
1725
#
1826
# So you can inspect reflections in your Adapters.
1927
#
20-
Reflection = Struct.new(:name, :options) do
28+
Reflection = Struct.new(:name, :options, :block) do
29+
delegate :call, to: :reader
30+
31+
attr_reader :reader
32+
33+
def initialize(*)
34+
super
35+
@reader = self.class.build_reader(name, block)
36+
end
37+
38+
# @api private
39+
def value(instance)
40+
call(instance)
41+
end
42+
43+
# @api private
44+
def self.build_reader(name, block)
45+
if block
46+
->(instance) { instance.read_attribute_for_serialization(name).instance_eval(&block) }
47+
else
48+
->(instance) { instance.read_attribute_for_serialization(name) }
49+
end
50+
end
51+
2152
# Build association. This method is used internally to
2253
# build serializer's association by its reflection.
2354
#
@@ -40,7 +71,7 @@ class Serializer
4071
# @api private
4172
#
4273
def build_association(subject, parent_serializer_options)
43-
association_value = subject.send(name)
74+
association_value = value(subject)
4475
reflection_options = options.dup
4576
serializer_class = subject.class.serializer_for(association_value, reflection_options)
4677

test/fixtures/poro.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def custom_options
116116
attributes :id, :name, :description, :slug
117117

118118
def slug
119-
"#{name}-#{id}"
119+
"#{object.name}-#{object.id}"
120120
end
121121

122122
belongs_to :author

0 commit comments

Comments
 (0)