Skip to content

Commit

Permalink
Document and minor refactors
Browse files Browse the repository at this point in the history
  • Loading branch information
oriolgual committed May 20, 2012
1 parent ef4ce7b commit 1656190
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 27 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'

54 changes: 42 additions & 12 deletions lib/hypermodel/resource.rb
@@ -1,34 +1,64 @@
require 'hypermodel/serializers/mongoid'
# This class is ORM agnostic. Wohoo!
# Next step is to select which fields to include in the output.

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
# TODO: Detect resource type (AR, DM, Mongoid, etc..) and create the
# corresponding serializer.
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)
@serializer.attributes.update(links).update(embedded).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
hash = { self: { href: @controller.polymorphic_url(@serializer.record) } }
@serializer.resources.each do |name, resource|
hash.update(name => {href: @controller.polymorphic_url(resource)})
_links = { self: polymorphic_url(record) }

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

@serializer.sub_resources.each do |sub_resource|
hash.update(sub_resource => {href: @controller.polymorphic_url([@serializer.record, sub_resource])})
sub_resources.each do |sub_resource|
_links.update(sub_resource => polymorphic_url([record, sub_resource]))
end

{ _links: hash }
{ _links: _links }
end

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

# Private: Returns the url wrapped in a Hash with in a HAL way.
def polymorphic_url(record_or_hash_or_array, options = {})
{ href: @controller.polymorphic_url(record_or_hash_or_array, options = {}) }
end
end
end
12 changes: 12 additions & 0 deletions lib/hypermodel/responder.rb
@@ -1,6 +1,18 @@
require 'hypermodel/resource'

module Hypermodel
# 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 Down
74 changes: 60 additions & 14 deletions lib/hypermodel/serializers/mongoid.rb
@@ -1,12 +1,37 @@
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
attr_reader :record, :attributes

# Public: Returns the Mongoid instance
attr_reader :record

# Public: Returns the attributes of the Mongoid instance
attr_reader :attributes

# Public: Initializes a Serializer::Mongoid.
#
# record - A Mongoid instance of a model.
def initialize(record)
@record = record
@record = record
@attributes = record.attributes.dup
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)

Expand All @@ -15,17 +40,41 @@ def resources
end
end

# 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

# 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
Expand All @@ -38,16 +87,13 @@ def select_relations_by_type(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
@record.send(name).map { |embedded| embedded.attributes }
elsif relation == ::Mongoid::Relations::Embedded::One
(embedded = @record.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
Expand Down

0 comments on commit 1656190

Please sign in to comment.