# frozen_string_literal: true
require 'active_model/array_serializer'
require 'active_model/serializable'
require 'active_model/serializer/association'
require 'active_model/serializer/config'
require 'thread'
require 'concurrent/map'
module ActiveModel
class Serializer
include Serializable
@mutex = Mutex.new
class << self
def inherited(base)
base._root = _root
base._attributes = (_attributes || []).dup
base._associations = (_associations || {}).dup
end
def setup
@mutex.synchronize do
yield CONFIG
end
end
EMBED_IN_ROOT_OPTIONS = [
:include,
:embed_in_root,
:embed_in_root_key,
:embed_namespace
].freeze
def embed(type, options={})
CONFIG.embed = type
if EMBED_IN_ROOT_OPTIONS.any? { |opt| options[opt].present? }
CONFIG.embed_in_root = true
end
if options[:embed_in_root_key].present?
CONFIG.embed_in_root_key = options[:embed_in_root_key]
end
ActiveSupport::Deprecation.warn <<-WARN
** Notice: embed is deprecated. **
The use of .embed method on a Serializer will be soon removed, as this should have a global scope and not a class scope.
Please use the global .setup method instead:
ActiveModel::Serializer.setup do |config|
config.embed = :#{type}
config.embed_in_root = #{CONFIG.embed_in_root || false}
end
WARN
end
def format_keys(format)
@key_format = format
end
attr_reader :key_format
def serializer_for(resource, options = {})
if resource.respond_to?(:serializer_class)
resource.serializer_class
elsif resource.respond_to?(:to_ary)
if defined?(::ArraySerializer)
::ArraySerializer
else
ArraySerializer
end
else
each_possible_serializer(resource, options) do |klass_name|
serializer = Serializer.serializers_cache.fetch_or_store(klass_name) do
_const_get(klass_name)
end
return serializer unless serializer.nil?
end
nil
end
end
attr_accessor :_root, :_attributes, :_associations
alias root _root=
alias root= _root=
def root_name
if name
root_name = name.demodulize.underscore.sub(/_serializer$/, '')
CONFIG.plural_default_root ? root_name.pluralize : root_name
end
end
def attributes(*attrs)
# Use `class_eval` rather than `define_method` for faster access
# and avoid retaining objects in the closure.
# Batch all methods in a single `class_eval` for efficiceny,
# and define all methods on the same line so the eventual backtrace
# properly maps to the `attributes` call.
source = ["# frozen_string_literal: true\n"]
attrs.each do |attr|
striped_attr = strip_attribute attr
@_attributes << striped_attr
unless method_defined?(attr)
source <<
"def #{striped_attr}" <<
"object.read_attribute_for_serialization(#{attr.inspect})" <<
"end" <<
"alias_method :#{striped_attr}, :#{striped_attr}" # suppress method redefinition warning
end
end
caller = caller_locations(1, 1).first
class_eval(source.join(";"), caller.path, caller.lineno - 1)
end
def has_one(*attrs)
associate(Association::HasOne, *attrs)
end
def has_many(*attrs)
associate(Association::HasMany, *attrs)
end
def serializers_cache
@serializers_cache ||= Concurrent::Map.new
end
private
def strip_attribute(attr)
symbolized = attr.is_a?(Symbol)
attr = attr.to_s.gsub(/\?\Z/, '')
attr = attr.to_sym if symbolized
attr
end
def each_possible_serializer(resource, options)
yield build_serializer_class(resource, options)
yield build_serializer_class(resource, {})
yield build_serializer_class(resource.class.name.demodulize, {})
end
def build_serializer_class(resource, options)
klass_name = +""
klass_name << "#{options[:namespace]}::" if options[:namespace]
klass_name << options[:prefix].to_s.classify if options[:prefix]
klass_name << "#{resource.class.name}Serializer"
end
def associate(klass, *attrs)
options = attrs.extract_options!
attrs.each do |attr|
define_method attr do
object.send attr
end unless method_defined?(attr)
@_associations[attr] = klass.new(attr, options)
end
end
end
def initialize(object, options={})
@object = object
@scope = options[:scope]
@root = options.fetch(:root, self.class._root)
@polymorphic = options.fetch(:polymorphic, false)
@meta_key = options[:meta_key] || :meta
@meta = options[@meta_key]
@wrap_in_array = options[:_wrap_in_array]
@only = options[:only] ? Array(options[:only]) : nil
@except = options[:except] ? Array(options[:except]) : nil
@key_format = options[:key_format]
@context = options[:context]
@namespace = options[:namespace]
end
attr_accessor :object, :scope, :root, :meta_key, :meta, :context, :polymorphic
attr_writer :key_format
def json_key
key = if root == true || root.nil?
self.class.root_name
else
root
end
key_format == :lower_camel && key.present? ? key.camelize(:lower) : key
end
def attributes
filter(self.class._attributes.dup).each_with_object({}) do |name, hash|
hash[name] = send(name)
end
end
def associations(options={})
associations = self.class._associations
included_associations = filter(associations.keys)
associations.each_with_object({}) do |(name, association), hash|
if included_associations.include? name
if association.embed_ids?
ids = serialize_ids association
if association.embed_namespace?
hash = hash[association.embed_namespace] ||= {}
hash[association.key] = ids
else
hash[association.key] = ids
end
elsif association.embed_objects?
if association.embed_namespace?
hash = hash[association.embed_namespace] ||= {}
end
hash[association.embedded_key] = serialize association, options
end
end
end
end
def filter(keys)
if @only
keys & @only
elsif @except
keys - @except
else
keys
end
end
def embedded_in_root_associations
associations = self.class._associations
included_associations = filter(associations.keys)
associations.each_with_object({}) do |(name, association), hash|
if included_associations.include? name
association_serializer = build_serializer(association)
# we must do this always because even if the current association is not
# embedded in root, it might have its own associations that are embedded in root
hash.merge!(association_serializer.embedded_in_root_associations) do |key, oldval, newval|
if oldval.respond_to?(:to_ary)
[oldval, newval].flatten.uniq
else
oldval.merge(newval) { |_, oldval, newval| [oldval, newval].flatten.uniq }
end
end
if association.embed_in_root?
if association.embed_in_root_key?
hash = hash[association.embed_in_root_key] ||= {}
end
serialized_data = association_serializer.serializable_object
key = association.root_key
if hash.has_key?(key)
hash[key].concat(serialized_data).uniq!
else
hash[key] = serialized_data
end
end
end
end
end
def build_serializer(association)
object = send(association.name)
association.build_serializer(object, association_options_for_serializer(association))
end
def association_options_for_serializer(association)
prefix = association.options[:prefix]
namespace = association.options[:namespace] || @namespace || self.namespace
{ scope: scope }.tap do |opts|
opts[:namespace] = namespace if namespace
opts[:prefix] = prefix if prefix
end
end
def serialize(association,options={})
build_serializer(association).serializable_object(options)
end
def serialize_ids(association)
associated_data = send(association.name)
if associated_data.respond_to?(:to_ary)
associated_data.map { |elem| serialize_id(elem, association) }
else
serialize_id(associated_data, association) if associated_data
end
end
def key_format
@key_format || self.class.key_format || CONFIG.key_format
end
def format_key(key)
if key_format == :lower_camel
key.to_s.camelize(:lower)
else
key
end
end
def convert_keys(hash)
Hash[hash.map do |k,v|
key = if k.is_a?(Symbol)
format_key(k).to_sym
else
format_key(k)
end
[key ,v]
end]
end
attr_writer :serialization_options
def serialization_options
@serialization_options || {}
end
def serializable_object(options={})
self.serialization_options = options
return @wrap_in_array ? [] : nil if @object.nil?
hash = attributes
hash.merge! associations(options)
hash = convert_keys(hash) if key_format.present?
hash = { :type => type_name(@object), type_name(@object) => hash } if @polymorphic
@wrap_in_array ? [hash] : hash
end
alias_method :serializable_hash, :serializable_object
def serialize_id(elem, association)
id = elem.read_attribute_for_serialization(association.embed_key)
association.polymorphic? ? { id: id, type: type_name(elem) } : id
end
def type_name(elem)
elem.class.to_s.demodulize.underscore.to_sym
end
end
end