Skip to content

Commit

Permalink
Associations refactoring
Browse files Browse the repository at this point in the history
* Move all associations related code from Serializer class to Associations module
* Introduce Reflection class hierarchy
* Introduce Association class
* Rid off Serializer#each_association
* Introduce Serializer#associations enumerator
  • Loading branch information
Артём Большаков committed Jul 7, 2015
1 parent 90fb1cf commit 0bc0247
Show file tree
Hide file tree
Showing 13 changed files with 381 additions and 175 deletions.
128 changes: 22 additions & 106 deletions lib/active_model/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
module ActiveModel
class Serializer
extend ActiveSupport::Autoload

autoload :Configuration
autoload :ArraySerializer
autoload :Adapter
autoload :Associations
include Configuration
include Associations

class << self
attr_accessor :_attributes
attr_accessor :_attributes_keys
attr_accessor :_associations
attr_accessor :_urls
attr_accessor :_cache
attr_accessor :_fragmented
Expand All @@ -23,12 +25,12 @@ class << self
end

def self.inherited(base)
base._attributes = self._attributes.try(:dup) || []
base._attributes = self._attributes.try(:dup) || []
base._attributes_keys = self._attributes_keys.try(:dup) || {}
base._associations = self._associations.try(:dup) || {}
base._urls = []
serializer_file = File.open(caller.first[/^[^:]+/])
base._cache_digest = Digest::MD5.hexdigest(serializer_file.read)
super
end

def self.attributes(*attrs)
Expand All @@ -45,7 +47,7 @@ def self.attributes(*attrs)

def self.attribute(attr, options = {})
key = options.fetch(:key, attr)
@_attributes_keys[attr] = {key: key} if key != attr
@_attributes_keys[attr] = { key: key } if key != attr
@_attributes << key unless @_attributes.include?(key)
define_method key do
object.read_attribute_for_serialization(attr)
Expand All @@ -58,58 +60,13 @@ def self.fragmented(serializer)

# Enables a serializer to be automatically cached
def self.cache(options = {})
@_cache = ActionController::Base.cache_store if Rails.configuration.action_controller.perform_caching
@_cache_key = options.delete(:key)
@_cache_only = options.delete(:only)
@_cache_except = options.delete(:except)
@_cache = ActionController::Base.cache_store if Rails.configuration.action_controller.perform_caching
@_cache_key = options.delete(:key)
@_cache_only = options.delete(:only)
@_cache_except = options.delete(:except)
@_cache_options = (options.empty?) ? nil : options
end

# Defines an association in the object should be rendered.
#
# The serializer object should implement the association name
# as a method which should return an array when invoked. If a method
# with the association name does not exist, the association name is
# dispatched to the serialized object.
def self.has_many(*attrs)
associate(:has_many, attrs)
end

# Defines an association in the object that should be rendered.
#
# The serializer object should implement the association name
# as a method which should return an object when invoked. If a method
# with the association name does not exist, the association name is
# dispatched to the serialized object.
def self.belongs_to(*attrs)
associate(:belongs_to, attrs)
end

# Defines an association in the object should be rendered.
#
# The serializer object should implement the association name
# as a method which should return an object when invoked. If a method
# with the association name does not exist, the association name is
# dispatched to the serialized object.
def self.has_one(*attrs)
associate(:has_one, attrs)
end

def self.associate(type, attrs) #:nodoc:
options = attrs.extract_options!
self._associations = _associations.dup

attrs.each do |attr|
unless method_defined?(attr)
define_method attr do
object.send attr
end
end

self._associations[attr] = {type: type, association_options: options}
end
end

def self.url(attr)
@_urls.push attr
end
Expand All @@ -124,19 +81,17 @@ def self.serializer_for(resource, options = {})
elsif resource.respond_to?(:to_ary)
config.array_serializer
else
options
.fetch(:association_options, {})
.fetch(:serializer, get_serializer_for(resource.class))
options.fetch(:serializer, get_serializer_for(resource.class))
end
end

def self.adapter
adapter_class = case config.adapter
when Symbol
ActiveModel::Serializer::Adapter.adapter_class(config.adapter)
when Class
config.adapter
end
when Symbol
ActiveModel::Serializer::Adapter.adapter_class(config.adapter)
when Class
config.adapter
end
unless adapter_class
valid_adapters = Adapter.constants.map { |klass| ":#{klass.to_s.downcase}" }
raise ArgumentError, "Unknown adapter: #{config.adapter}. Valid adapters are: #{valid_adapters}"
Expand All @@ -152,12 +107,12 @@ def self.root_name
attr_accessor :object, :root, :meta, :meta_key, :scope

def initialize(object, options = {})
@object = object
@options = options
@root = options[:root]
@meta = options[:meta]
@meta_key = options[:meta_key]
@scope = options[:scope]
@object = object
@options = options
@root = options[:root]
@meta = options[:meta]
@meta_key = options[:meta_key]
@scope = options[:scope]

scope_name = options[:scope_name]
if scope_name && !respond_to?(scope_name)
Expand Down Expand Up @@ -198,48 +153,10 @@ def attributes(options = {})
end
end

def each_association(&block)
self.class._associations.dup.each do |name, association_options|
next unless object
association_value = send(name)

serializer_class = ActiveModel::Serializer.serializer_for(association_value, association_options)

if serializer_class
begin
serializer = serializer_class.new(
association_value,
options.except(:serializer).merge(serializer_from_options(association_options))
)
rescue ActiveModel::Serializer::ArraySerializer::NoSerializerError
virtual_value = association_value
virtual_value = virtual_value.as_json if virtual_value.respond_to?(:as_json)
association_options[:association_options][:virtual_value] = virtual_value
end
elsif !association_value.nil? && !association_value.instance_of?(Object)
association_options[:association_options][:virtual_value] = association_value
end

association_key = association_options[:association_options][:key] || name
if block_given?
block.call(association_key, serializer, association_options[:association_options])
end
end
end

def serializer_from_options(options)
opts = {}
serializer = options.fetch(:association_options, {}).fetch(:serializer, nil)
opts[:serializer] = serializer if serializer
opts
end

def self.serializers_cache
@serializers_cache ||= ThreadSafe::Cache.new
end

private

attr_reader :options

def self.get_serializer_for(klass)
Expand All @@ -254,6 +171,5 @@ def self.get_serializer_for(klass)
end
end
end

end
end
28 changes: 15 additions & 13 deletions lib/active_model/serializer/adapter/json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,34 @@ class Json < Adapter
def serializable_hash(options = nil)
options ||= {}
if serializer.respond_to?(:each)
@result = serializer.map{|s| FlattenJson.new(s).serializable_hash(options) }
@result = serializer.map { |s| FlattenJson.new(s).serializable_hash(options) }
else
@hash = {}

@core = cache_check(serializer) do
serializer.attributes(options)
end

serializer.each_association do |key, association, opts|
if association.respond_to?(:each)
array_serializer = association
@hash[key] = array_serializer.map do |item|
serializer.associations.each do |association|
serializer = association.serializer
opts = association.options

if serializer.respond_to?(:each)
array_serializer = serializer
@hash[association.key] = array_serializer.map do |item|
cache_check(item) do
item.attributes(opts)
end
end
else
if association && association.object
@hash[key] = cache_check(association) do
association.attributes(options)
@hash[association.key] =
if serializer && serializer.object
cache_check(serializer) do
serializer.attributes(options)
end
elsif opts[:virtual_value]
opts[:virtual_value]
end
elsif opts[:virtual_value]
@hash[key] = opts[:virtual_value]
else
@hash[key] = nil
end
end
end
@result = @core.merge @hash
Expand Down
22 changes: 14 additions & 8 deletions lib/active_model/serializer/adapter/json_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,10 @@ def add_included(resource_name, serializers, parent = nil)
end

serializers.each do |serializer|
serializer.each_association do |key, association, opts|
add_included(key, association, resource_path) if association
serializer.associations.each do |association|
serializer = association.serializer

add_included(association.key, serializer, resource_path) if serializer
end if include_nested_assoc? resource_path
end
end
Expand Down Expand Up @@ -131,22 +133,26 @@ def check_assoc(assoc)
def add_resource_relationships(attrs, serializer, options = {})
options[:add_included] = options.fetch(:add_included, true)

serializer.each_association do |key, association, opts|
serializer.associations.each do |association|
key = association.key
serializer = association.serializer
opts = association.options

attrs[:relationships] ||= {}

if association.respond_to?(:each)
add_relationships(attrs, key, association)
if serializer.respond_to?(:each)
add_relationships(attrs, key, serializer)
else
if opts[:virtual_value]
add_relationship(attrs, key, nil, opts[:virtual_value])
else
add_relationship(attrs, key, association)
add_relationship(attrs, key, serializer)
end
end

if options[:add_included]
Array(association).each do |association|
add_included(key, association)
Array(serializer).each do |serializer|
add_included(key, serializer)
end
end
end
Expand Down
21 changes: 21 additions & 0 deletions lib/active_model/serializer/association.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module ActiveModel
class Serializer
# This class hold all information about serializer's association.
#
# @param [Symbol] name
# @param [ActiveModel::Serializer] serializer
# @param [Hash{Symbol => Object}] options
#
# @example
# Association.new(:comments, CommentSummarySerializer, embed: :ids)
#
Association = Struct.new(:name, :serializer, :options) do

# @return [Symbol]
#
def key
options.fetch(:key, name)
end
end
end
end
Loading

0 comments on commit 0bc0247

Please sign in to comment.