Skip to content

Loading…

Refactor serializer #1

Merged
merged 6 commits into from

2 participants

@oriolgual
Codegram member

Extract a Resource class from the serializer so we can later personalize it.

Add documentation to classes and refactor for cleaner code.

@oriolgual
Codegram member

I demand your attention @josepjaume, @txus, @divins, @mrcasals

@txus
Codegram member

UOOOOOOOO mind = blown!!

@oriolgual oriolgual merged commit 69c4d63 into master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on May 18, 2012
  1. @oriolgual
  2. @oriolgual
  3. @oriolgual

    Try a better naming

    oriolgual committed
  4. @oriolgual

    Refactor resources extraction

    oriolgual committed
Commits on May 20, 2012
  1. @oriolgual

    Document and minor refactors

    oriolgual committed
  2. @oriolgual

    Typo

    oriolgual committed
This page is out of date. Refresh to see the latest.
Showing with 156 additions and 45 deletions.
  1. +2 −1 lib/hypermodel.rb
  2. +64 −0 lib/hypermodel/resource.rb
  3. +15 −5 lib/hypermodel/responder.rb
  4. +75 −39 lib/hypermodel/serializers/mongoid.rb
View
3 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'
-
View
64 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
View
20 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]
@@ -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)
View
114 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
Something went wrong with that request. Please try again.