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
|