-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
reflection.rb
212 lines (197 loc) · 6.79 KB
/
reflection.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# frozen_string_literal: true
require 'active_model/serializer/field'
require 'active_model/serializer/association'
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
# has_one :author, serializer: AuthorSerializer
# belongs_to :boss, type: :users, foreign_key: :boss_id
# has_many :comments
# has_many :comments, key: :last_comments do
# object.comments.last(1)
# end
# has_many :secret_meta_data, if: :is_admin?
#
# has_one :blog do |serializer|
# meta count: object.roles.count
# serializer.cached_blog
# end
#
# private
#
# def cached_blog
# cache_store.fetch("cached_blog:#{object.updated_at}") do
# Blog.find(object.blog_id)
# end
# end
#
# def is_admin?
# current_user.admin?
# end
# end
#
# 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'.
#
# PostSerializer._reflections # =>
# # {
# # author: HasOneReflection.new(:author, serializer: AuthorSerializer),
# # comments: HasManyReflection.new(:comments)
# # last_comments: HasManyReflection.new(:comments, { key: :last_comments }, #<Block>)
# # secret_meta_data: HasManyReflection.new(:secret_meta_data, { if: :is_admin? })
# # }
#
# So you can inspect reflections in your Adapters.
class Reflection < Field
attr_reader :foreign_key, :type
def initialize(*)
super
options[:links] = {}
options[:include_data_setting] = Serializer.config.include_data_default
options[:meta] = nil
@type = options.fetch(:type) do
class_name = options.fetch(:class_name, name.to_s.camelize.singularize)
class_name.underscore.pluralize.to_sym
end
@foreign_key = options.fetch(:foreign_key) do
if collection?
"#{name.to_s.singularize}_ids".to_sym
else
"#{name}_id".to_sym
end
end
end
# @api public
# @example
# has_one :blog do
# include_data false
# link :self, 'a link'
# link :related, 'another link'
# link :self, '//example.com/link_author/relationships/bio'
# id = object.profile.id
# link :related do
# "//example.com/profiles/#{id}" if id != 123
# end
# link :related do
# ids = object.likes.map(&:id).join(',')
# href "//example.com/likes/#{ids}"
# meta ids: ids
# end
# end
def link(name, value = nil, &block)
options[:links][name] = block_given? ? block : value
:nil
end
# @api public
# @example
# has_one :blog do
# include_data false
# meta(id: object.blog.id)
# meta liked: object.likes.any?
# link :self do
# href object.blog.id.to_s
# meta(id: object.blog.id)
# end
def meta(value = nil, &block)
options[:meta] = block_given? ? block : value
:nil
end
# @api public
# @example
# has_one :blog do
# include_data false
# link :self, 'a link'
# link :related, 'another link'
# end
#
# has_one :blog do
# include_data false
# link :self, 'a link'
# link :related, 'another link'
# end
#
# belongs_to :reviewer do
# meta name: 'Dan Brown'
# include_data true
# end
#
# has_many :tags, serializer: TagSerializer do
# link :self, '//example.com/link_author/relationships/tags'
# include_data :if_sideloaded
# end
def include_data(value = true)
options[:include_data_setting] = value
:nil
end
def collection?
false
end
def include_data?(include_slice)
include_data_setting = options[:include_data_setting]
case include_data_setting
when :if_sideloaded then include_slice.key?(options.fetch(:key, name))
when true then true
when false then false
else fail ArgumentError, "Unknown include_data_setting '#{include_data_setting.inspect}'"
end
end
# @param serializer [ActiveModel::Serializer]
# @yield [ActiveModel::Serializer]
# @return [:nil, associated resource or resource collection]
def value(serializer, include_slice)
# NOTE(BF): This method isn't thread-safe because the _reflections class attribute is not thread-safe
# Therefore, when we build associations from reflections, we dup the entire reflection instance.
# Better solutions much appreciated!
@object = serializer.object
@scope = serializer.scope
block_value = instance_exec(serializer, &block) if block
return unless include_data?(include_slice)
if block && block_value != :nil
block_value
else
serializer.read_attribute_for_serialization(name)
end
end
# @api private
def foreign_key_on
:related
end
# Build association. This method is used internally to
# build serializer's association by its reflection.
#
# @param [Serializer] parent_serializer for given association
# @param [Hash{Symbol => Object}] parent_serializer_options
#
# @example
# # Given the following serializer defined:
# class PostSerializer < ActiveModel::Serializer
# has_many :comments, serializer: CommentSummarySerializer
# end
#
# # Then you instantiate your serializer
# post_serializer = PostSerializer.new(post, foo: 'bar') #
# # to build association for comments you need to get reflection
# comments_reflection = PostSerializer._reflections.detect { |r| r.name == :comments }
# # and #build_association
# comments_reflection.build_association(post_serializer, foo: 'bar')
#
# @api private
def build_association(parent_serializer, parent_serializer_options, include_slice = {})
association_options = {
parent_serializer: parent_serializer,
parent_serializer_options: parent_serializer_options,
include_slice: include_slice
}
Association.new(self, association_options)
end
protected
# used in instance exec
attr_accessor :object, :scope
end
end
end