Skip to content

Commit

Permalink
Merge pull request #1 from codegram/extract_resource
Browse files Browse the repository at this point in the history
Refactor serializer
  • Loading branch information
oriolgual committed May 20, 2012
2 parents 00b1280 + 497a272 commit 69c4d63
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 45 deletions.
3 changes: 2 additions & 1 deletion lib/hypermodel.rb
@@ -1,5 +1,6 @@
# Public: A Hypermodel is a representation of a resource in a JSON-HAL format.
# To learn more about JSON HAL see http://stateless.co/hal_specification.html
module Hypermodel
end

require 'hypermodel/responder'

64 changes: 64 additions & 0 deletions lib/hypermodel/resource.rb
@@ -0,0 +1,64 @@
require 'hypermodel/serializers/mongoid'

module Hypermodel
# Public: Responsible for building the response in JSON-HAL format. It is
# meant to be used by Hypermodel::Responder.
#
# In future versions one will be able to subclass it and personalize a
# Resource for each diffent model, i.e. creating a PostResource.
class Resource
extend Forwardable

def_delegators :@serializer, :attributes, :record, :resources,
:sub_resources, :embedded_resources

# Public: Initializes a Resource.
#
# record - A Mongoid instance of a model.
# controller - An ActionController instance.
#
# TODO: Detect record type (ActiveRecord, DataMapper, Mongoid, etc..) and
# choose the corresponding serializer.
def initialize(record, controller)
@serializer = Serializers::Mongoid.new(record)
@controller = controller
end

# Public: Returns a Hash of the resource in JSON-HAL.
#
# opts - Options to pass to the resource to_json.
def to_json(*opts)
attributes.update(links).update(embedded).to_json(*opts)
end

# Private: Constructs the _links section of the response.
#
# Returns: A Hash of the links of the resource. It will include, at least,
# a link to itself.
def links
_links = { self: polymorphic_url(record) }

resources.each do |name, resource|
_links.update(name => polymorphic_url(resource))
end

sub_resources.each do |sub_resource|
_links.update(sub_resource => polymorphic_url([record, sub_resource]))
end

{ _links: _links }
end

# Private: Constructs the _embedded section of the response.
#
# Returns: A Hash of the embedded resources of the resource.
def embedded
{ _embedded: embedded_resources }
end

# Private: Returns the url wrapped in a Hash in HAL format.
def polymorphic_url(record_or_hash_or_array, options = {})
{ href: @controller.polymorphic_url(record_or_hash_or_array, options = {}) }
end
end
end
20 changes: 15 additions & 5 deletions lib/hypermodel/responder.rb
@@ -1,8 +1,18 @@
require 'hypermodel/serializers/mongoid'
require 'hypermodel/resource'

module Hypermodel
Serializer = Serializers::Mongoid

# Public: Responsible for exposing a resource in JSON-HAL format.
#
# Examples
#
# class PostsController < ApplicationController
# respond_to :json
#
# def show
# @post = Post.find params[:id]
# respond_with(@post, responder: Hypermodel::Responder)
# end
# end
class Responder
def self.call(*args)
controller = args[0]
Expand All @@ -15,10 +25,10 @@ def self.call(*args)
controller.render json: responder
end

def initialize(resource_name, action, resource, controller)
def initialize(resource_name, action, record, controller)
@resource_name = resource_name
@action = action
@resource = Serializer.new(resource, controller)
@resource = Resource.new(record, controller)
end

def to_json(*opts)
Expand Down
114 changes: 75 additions & 39 deletions lib/hypermodel/serializers/mongoid.rb
@@ -1,73 +1,109 @@
module Hypermodel
module Serializers
# Private: A Mongoid serializer that complies with the Hypermodel
# Serializer API.
#
# It is used by Hypermodel::Resource to extract the attributes and
# resources of a given record.
class Mongoid
def initialize(resource, controller)
@resource = resource
@attributes = resource.attributes.dup
@controller = controller
end

def to_json(*opts)
attrs = @attributes.update(links).update(embedded)
attrs.to_json(*opts)
end
# Public: Returns the Mongoid instance
attr_reader :record

def links
hash = { self: { href: @controller.polymorphic_url(@resource) } }
# Public: Returns the attributes of the Mongoid instance
attr_reader :attributes

referenced_relations.each do |name, metadata|
related = @resource.send(name)
relation = metadata.relation
# Public: Initializes a Serializer::Mongoid.
#
# record - A Mongoid instance of a model.
def initialize(record)
@record = record
@attributes = record.attributes.dup
end

if relation == ::Mongoid::Relations::Referenced::In
unless related.nil? || (related.respond_to?(:empty?) && related.empty?)
hash.update(name => { href: @controller.polymorphic_url(related) })
end
elsif relation == ::Mongoid::Relations::Referenced::Many
hash.update(name => { href: @controller.polymorphic_url([@resource, name]) })
else
raise "Referenced relation type not implemented: #{relation}"
end
# Public: Returns a Hash with the resources that are linked to the
# record. It will be used by Hypermodel::Resource.
#
# An example of a linked resource could be the author of a post. Think of
# `/authors/:author_id`
#
# The format of the returned Hash must be the following:
#
# {resource_name: resource_instance}
#
# `resource_name` can be either a Symbol or a String.
def resources
relations = select_relations_by_type(::Mongoid::Relations::Referenced::In)

relations.inject({}) do |acc, (name, _)|
acc.update(name => @record.send(name))
end
end

{ _links: hash }
# Public: Returns a Hash with the sub resources that are linked to the
# record. It will be used by Hypermodel::Resource. These resources need
# to be differentiated so Hypermodel::Resource can build the url.
#
# An example of a linked sub resource could be comments of a post.
# Think of `/posts/:id/comments`
#
# The format of the returned Hash must be the following:
#
# {:sub_resource, :another_subresource}
def sub_resources
select_relations_by_type(::Mongoid::Relations::Referenced::Many).keys
end

def embedded
# Public: Returns a Hash with the embedded resources attributes. It will
# be used by Hypermodel::Resource.
#
# An example of an embedded resource could be the reviews of a post, or
# the addresses of a company. But you can really embed whatever you like.
#
# An example of the returning Hash could be the following:
#
# {"comments"=>
# [
# {"_id"=>"4fb941cb82b4d46162000007", "body"=>"Comment 1"},
# {"_id"=>"4fb941cb82b4d46162000008", "body"=>"Comment 2"}
# ]
# }
def embedded_resources
return {} if embedded_relations.empty?

embedded_relations.inject({ _embedded: {} }) do |acc, (name, metadata)|
if attributes = extract_embedded_attributes(name, metadata)
embedded_relations.inject({}) do |acc, (name, metadata)|
if attributes = extract_embedded_attributes(name)
@attributes.delete(name)
acc[:_embedded][name] = attributes
acc.update(name => attributes)
end
acc
end
end

private
def select_relations_by_type(type)
referenced_relations.select do |name, metadata|
metadata.relation == type
end
end

def extract_embedded_attributes(name, metadata)
relation = metadata.relation
def extract_embedded_attributes(name)
embedded = @record.send(name)

if relation == ::Mongoid::Relations::Embedded::Many
@resource.send(name).map { |embedded| embedded.attributes }
elsif relation == ::Mongoid::Relations::Embedded::One
(embedded = resource.send(name)) ? embedded.attributes : nil
else
raise "Embedded relation type not implemented: #{relation}"
end
return {} unless embedded
return embedded.map(&:attributes) if embedded.respond_to?(:map)

embedded.attributes
end

def embedded_relations
@embedded_relations ||= @resource.relations.select do |_, metadata|
@embedded_relations ||= @record.relations.select do |_, metadata|
metadata.relation.name =~ /Embedded/
end
end

def referenced_relations
@referenced_relations ||= @resource.relations.select do |_, metadata|
@referenced_relations ||= @record.relations.select do |_, metadata|
metadata.relation.name =~ /Referenced/
end
end
Expand Down

0 comments on commit 69c4d63

Please sign in to comment.