Skip to content

Move serialization logic into Serializer and CollectionSerializer #1766

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 6, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 53 additions & 8 deletions lib/active_model/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,22 @@ def self.get_serializer_for(klass)
end
end

# @api private
def self.include_directive_from_options(options)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing @api private. These methods are being held here for now, but may move around, be renamed, or disappear

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably go in AMS or some other serialization-related class

if options[:include_directive]
options[:include_directive]
elsif options[:include]
JSONAPI::IncludeDirective.new(options[:include], allow_wildcard: true)
else
ActiveModelSerializers.default_include_directive
end
end

# @api private
def self.serialization_adapter_instance
@serialization_adapter_instance ||= ActiveModelSerializers::Adapter::Attributes
end

attr_accessor :object, :root, :scope

# `scope_name` is set as :current_user by default in the controller.
Expand All @@ -123,9 +139,7 @@ def success?
# associations, similar to how ActiveModel::Serializers::JSON is used
# in ActiveRecord::Base.
#
# TODO: Move to here the Attributes adapter logic for
# +serializable_hash_for_single_resource(options)+
# and include <tt>ActiveModel::Serializers::JSON</tt>.
# TODO: Include <tt>ActiveModel::Serializers::JSON</tt>.
# So that the below is true:
# @param options [nil, Hash] The same valid options passed to `serializable_hash`
# (:only, :except, :methods, and :include).
Expand All @@ -149,11 +163,13 @@ def success?
# serializer.as_json(include: :posts)
# # Second level and higher order associations work as well:
# serializer.as_json(include: { posts: { include: { comments: { only: :body } }, only: :title } })
def serializable_hash(adapter_opts = nil)
adapter_opts ||= {}
adapter_opts = { include: '*', adapter: :attributes }.merge!(adapter_opts)
adapter = ActiveModelSerializers::Adapter.create(self, adapter_opts)
adapter.serializable_hash(adapter_opts)
def serializable_hash(adapter_options = nil, options = {}, adapter_instance = self.class.serialization_adapter_instance)
adapter_options ||= {}
options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options)
cached_attributes = adapter_options[:cached_attributes] ||= {}
resource = cached_attributes(options[:fields], cached_attributes, adapter_instance)
relationships = resource_relationships(adapter_options, options, adapter_instance)
resource.merge(relationships)
end
alias to_hash serializable_hash
alias to_h serializable_hash
Expand Down Expand Up @@ -185,6 +201,35 @@ def read_attribute_for_serialization(attr)
end
end

# @api private
def resource_relationships(adapter_options, options, adapter_instance)
relationships = {}
include_directive = options.fetch(:include_directive)
associations(include_directive).each do |association|
adapter_opts = adapter_options.merge(include_directive: include_directive[association.key])
relationships[association.key] ||= relationship_value_for(association, adapter_opts, adapter_instance)
end

relationships
end

# @api private
def relationship_value_for(association, adapter_options, adapter_instance)
return association.options[:virtual_value] if association.options[:virtual_value]
association_serializer = association.serializer
association_object = association_serializer && association_serializer.object
return unless association_object

relationship_value = association_serializer.serializable_hash(adapter_options, {}, adapter_instance)

if association.options[:polymorphic] && relationship_value
polymorphic_type = association_object.class.name.underscore
relationship_value = { type: polymorphic_type, polymorphic_type.to_sym => relationship_value }
end

relationship_value
end

protected

attr_accessor :instance_options
Expand Down
2 changes: 1 addition & 1 deletion lib/active_model/serializer/caching.rb
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ def cache_key(adapter_instance)

parts = []
parts << object_cache_key
parts << adapter_instance.cached_name
parts << adapter_instance.cache_key
parts << self.class._cache_digest unless self.class._skip_digest?
@cache_key = parts.join('/')
end
Expand Down
40 changes: 30 additions & 10 deletions lib/active_model/serializer/collection_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,23 @@ def initialize(resources, options = {})
@object = resources
@options = options
@root = options[:root]
serializer_context_class = options.fetch(:serializer_context_class, ActiveModel::Serializer)
@serializers = resources.map do |resource|
serializer_class = options.fetch(:serializer) { serializer_context_class.serializer_for(resource) }

if serializer_class.nil? # rubocop:disable Style/GuardClause
fail NoSerializerError, "No serializer found for resource: #{resource.inspect}"
else
serializer_class.new(resource, options.except(:serializer))
end
end
@serializers = serializers_from_resources
end

def success?
true
end

# @api private
def serializable_hash(adapter_options, options, adapter_instance)
include_directive = ActiveModel::Serializer.include_directive_from_options(adapter_options)
adapter_options[:cached_attributes] ||= ActiveModel::Serializer.cache_read_multi(self, adapter_instance, include_directive)
adapter_opts = adapter_options.merge(include_directive: include_directive)
serializers.map do |serializer|
serializer.serializable_hash(adapter_opts, options, adapter_instance)
end
end

# TODO: unify naming of root, json_key, and _type. Right now, a serializer's
# json_key comes from the root option or the object's model name, by default.
# But, if a dev defines a custom `json_key` method with an explicit value,
Expand Down Expand Up @@ -59,6 +60,25 @@ def paginated?
protected

attr_reader :serializers, :options

private

def serializers_from_resources
serializer_context_class = options.fetch(:serializer_context_class, ActiveModel::Serializer)
object.map do |resource|
serializer_from_resource(resource, serializer_context_class, options)
end
end

def serializer_from_resource(resource, serializer_context_class, options)
serializer_class = options.fetch(:serializer) { serializer_context_class.serializer_for(resource) }

if serializer_class.nil? # rubocop:disable Style/GuardClause
fail NoSerializerError, "No serializer found for resource: #{resource.inspect}"
else
serializer_class.new(resource, options.except(:serializer))
end
end
end
end
end
4 changes: 4 additions & 0 deletions lib/active_model_serializers/adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ def register(name, klass = name)
self
end

def registered_name(adapter_class)
ADAPTER_MAP.key adapter_class
end

# @param adapter [String, Symbol, Class] name to fetch adapter by
# @return [ActiveModelSerializers::Adapter] subclass of Adapter
# @raise [UnknownAdapterError]
Expand Down
58 changes: 1 addition & 57 deletions lib/active_model_serializers/adapter/attributes.rb
Original file line number Diff line number Diff line change
@@ -1,65 +1,9 @@
module ActiveModelSerializers
module Adapter
class Attributes < Base
def initialize(serializer, options = {})
super
@include_directive =
if options[:include_directive]
options[:include_directive]
elsif options[:include]
JSONAPI::IncludeDirective.new(options[:include], allow_wildcard: true)
else
ActiveModelSerializers.default_include_directive
end
end

def serializable_hash(options = nil)
options = serialization_options(options)

if serializer.respond_to?(:each)
serializable_hash_for_collection(options)
else
serializable_hash_for_single_resource(options)
end
end

private

def serializable_hash_for_collection(options)
instance_options[:cached_attributes] ||= ActiveModel::Serializer.cache_read_multi(serializer, self, @include_directive)
opts = instance_options.merge(include_directive: @include_directive)
serializer.map { |s| Attributes.new(s, opts).serializable_hash(options) }
end

def serializable_hash_for_single_resource(options)
cached_attributes = instance_options[:cached_attributes] || {}
resource = serializer.cached_attributes(options[:fields], cached_attributes, self)
relationships = resource_relationships(options)
resource.merge(relationships)
end

def resource_relationships(options)
relationships = {}
serializer.associations(@include_directive).each do |association|
relationships[association.key] ||= relationship_value_for(association, options)
end

relationships
end

def relationship_value_for(association, options)
return association.options[:virtual_value] if association.options[:virtual_value]
return unless association.serializer && association.serializer.object

opts = instance_options.merge(include_directive: @include_directive[association.key])
relationship_value = Attributes.new(association.serializer, opts).serializable_hash(options)

if association.options[:polymorphic] && relationship_value
polymorphic_type = association.serializer.object.class.name.underscore
relationship_value = { type: polymorphic_type, polymorphic_type.to_sym => relationship_value }
end

relationship_value
serializer.serializable_hash(instance_options, options, self)
end
end
end
Expand Down
72 changes: 39 additions & 33 deletions lib/active_model_serializers/adapter/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,47 @@ def self.inherited(subclass)
ActiveModelSerializers::Adapter.register(subclass)
end

# Sets the default transform for the adapter.
#
# @return [Symbol] the default transform for the adapter
def self.default_key_transform
:unaltered
end

# Determines the transform to use in order of precedence:
# adapter option, global config, adapter default.
#
# @param options [Object]
# @return [Symbol] the transform to use
def self.transform(options)
return options[:key_transform] if options && options[:key_transform]
ActiveModelSerializers.config.key_transform || default_key_transform
end

# Transforms the casing of the supplied value.
#
# @param value [Object] the value to be transformed
# @param options [Object] serializable resource options
# @return [Symbol] the default transform for the adapter
def self.transform_key_casing!(value, options)
KeyTransform.send(transform(options), value)
end

def self.cache_key
@cache_key ||= ActiveModelSerializers::Adapter.registered_name(self)
end

def self.fragment_cache(cached_hash, non_cached_hash)
non_cached_hash.merge cached_hash
end

attr_reader :serializer, :instance_options

def initialize(serializer, options = {})
@serializer = serializer
@instance_options = options
end

def cached_name
@cached_name ||= self.class.name.demodulize.underscore
end

# Subclasses that implement this method must first call
# options = serialization_options(options)
def serializable_hash(_options = nil)
Expand All @@ -29,8 +59,12 @@ def as_json(options = nil)
serializable_hash(options)
end

def cache_key
self.class.cache_key
end

def fragment_cache(cached_hash, non_cached_hash)
non_cached_hash.merge cached_hash
self.class.fragment_cache(cached_hash, non_cached_hash)
end

private
Expand All @@ -44,34 +78,6 @@ def serialization_options(options)
def root
serializer.json_key.to_sym if serializer.json_key
end

class << self
# Sets the default transform for the adapter.
#
# @return [Symbol] the default transform for the adapter
def default_key_transform
:unaltered
end

# Determines the transform to use in order of precedence:
# adapter option, global config, adapter default.
#
# @param options [Object]
# @return [Symbol] the transform to use
def transform(options)
return options[:key_transform] if options && options[:key_transform]
ActiveModelSerializers.config.key_transform || default_key_transform
end

# Transforms the casing of the supplied value.
#
# @param value [Object] the value to be transformed
# @param options [Object] serializable resource options
# @return [Symbol] the default transform for the adapter
def transform_key_casing!(value, options)
KeyTransform.send(transform(options), value)
end
end
end
end
end
36 changes: 20 additions & 16 deletions lib/active_model_serializers/adapter/json_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,27 @@ class JsonApi < Base
autoload :Error
autoload :Deserialization

def self.default_key_transform
:dash
end

def self.fragment_cache(cached_hash, non_cached_hash, root = true)
core_cached = cached_hash.first
core_non_cached = non_cached_hash.first
no_root_cache = cached_hash.delete_if { |key, _value| key == core_cached[0] }
no_root_non_cache = non_cached_hash.delete_if { |key, _value| key == core_non_cached[0] }
cached_resource = (core_cached[1]) ? core_cached[1].deep_merge(core_non_cached[1]) : core_non_cached[1]
hash = root ? { root => cached_resource } : cached_resource

hash.deep_merge no_root_non_cache.deep_merge no_root_cache
end

def initialize(serializer, options = {})
super
@include_directive = JSONAPI::IncludeDirective.new(options[:include], allow_wildcard: true)
@fieldset = options[:fieldset] || ActiveModel::Serializer::Fieldset.new(options.delete(:fields))
end

def self.default_key_transform
:dash
end

# {http://jsonapi.org/format/#crud Requests are transactional, i.e. success or failure}
# {http://jsonapi.org/format/#document-top-level data and errors MUST NOT coexist in the same document.}
def serializable_hash(*)
Expand All @@ -52,6 +63,11 @@ def serializable_hash(*)
self.class.transform_key_casing!(document, instance_options)
end

def fragment_cache(cached_hash, non_cached_hash)
root = !instance_options.include?(:include)
self.class.fragment_cache(cached_hash, non_cached_hash, root)
end

# {http://jsonapi.org/format/#document-top-level Primary data}
# definition:
# ☐ toplevel_data (required)
Expand Down Expand Up @@ -174,18 +190,6 @@ def failure_document
hash
end

def fragment_cache(cached_hash, non_cached_hash)
root = false if instance_options.include?(:include)
core_cached = cached_hash.first
core_non_cached = non_cached_hash.first
no_root_cache = cached_hash.delete_if { |key, _value| key == core_cached[0] }
no_root_non_cache = non_cached_hash.delete_if { |key, _value| key == core_non_cached[0] }
cached_resource = (core_cached[1]) ? core_cached[1].deep_merge(core_non_cached[1]) : core_non_cached[1]
hash = root ? { root => cached_resource } : cached_resource

hash.deep_merge no_root_non_cache.deep_merge no_root_cache
end

protected

attr_reader :fieldset
Expand Down
Loading