Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Serializers
module ApiMe
module Serializers
class Base
attr_reader :object
def self.serializer_meta
@serializer_meta ||= SerializerMeta.new
end
def self.has_many(name, options = {})
descriptor = HasManyDescriptor.new(name, options)
serializer_meta.add_has_many(descriptor)
end
def self.attributes(*attrs)
attrs.each { |attr| serializer_meta.add_attribute(attr) }
end
def initialize(object)
@object = object
end
def batch_relations
batch_has_many
self
end
def batch_has_many
@lazy_has_many_relations = self.class.serializer_meta.has_many_relations.map do |relation|
{ relation.name => relation.batch(self.object) }
end
end
def hash_has_many_relations
@lazy_has_many_relations.reduce({}) { |relations_hash, lazy_hash|
relations_hash.merge!(lazy_hash)
}
end
def hash_attributes
self.class.serializer_meta.attributes.reduce({}) do |hash, attr|
if self.respond_to?(attr)
hash.merge!(attr => self.send(attr))
else
hash.merge!(attr => object.send(attr))
end
end
end
def serializable_hash
hash_has_many_relations.merge!(hash_attributes)
end
def as_json
serializable_hash.as_json
end
end
end
end
class CollectionSerializer
attr_reader :collection
def initialize(collection)
@collection = collection
end
def get_serializer(resource)
if resource.respond_to?(:serializer_class)
resource.serializer_class
else
resource_class = resource.class == Class ? resource : resource.class
"#{resource_class}Serializer".constantize
end
end
def build_serializer(resource)
get_serializer(resource).new(resource)
end
def serializable_hash
collection.map { |r| build_serializer(r).batch_relations.serializable_hash }
end
def as_json
serializable_hash.as_json
end
end
# Example use of code above. In practice, `collection_serializer.as_json` would
# automatically get called by the Rails hook. It would also need to inject the `user`
# because serializers can do different serialization based off of user access. It
# would also probably go through an adapter that would call `as_json` on the collection
# so that it could potentailly pass in includes or similar fields to limit serialization
# based off of only what the REST request wants.
collection = Post.all
collection_serializer = CollectionSerializer.new(collection)
puts collection_serializer.as_json
class HasManyDescriptor
attr_reader :name, :table_name, :inverse_of, :class_name
def initialize(name, table_name: nil, inverse_of: nil, class_name: nil)
@name = name
@class_name = class_name || name.to_s.classify.constantize
@inverse_of = inverse_of
end
end
require 'batch-loader'
class HasManyRelation
attr_reader :descriptor
def initialize(descriptor)
@descriptor = descriptor
end
def relation_class
descriptor.class_name
end
def table_name
relation_class.table_name
end
def name
descriptor.name
end
def batch(object)
inverse_name = descriptor.inverse_of || object.class.table_name
BatchLoader.for(object.id).batch do |relation_ids, loader|
relation_class.joins(inverse_name.to_sym)
.where(id: relation_ids)
.pluck("#{object.class.table_name}.id", "#{table_name}.id")
.group_by { |set| set[0] }
.each_pair do |key, value|
loader.call(key, value.map { |set| set[1] })
end
end
end
end
# An Example serializer
class PostSerializer < ApiMe::Serializers::Base
has_many :tags
attributes :name
end
class SerializerMeta
def initialize
@has_many_relationships = []
@attributes = []
end
def add_has_many(descriptor)
@has_many_relationships.push(HasManyRelation.new(descriptor))
end
def add_attribute(name)
@attributes.push(name)
end
def has_many_relations
@has_many_relationships.each
end
def attributes
@attributes.each
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment