Permalink
Browse files

Make hypermodel support collections

  • Loading branch information...
1 parent 0a93638 commit 52bf5c06c75012d2edea8eb53ae6ee2cb52cef77 @txus txus committed May 24, 2012
@@ -0,0 +1,93 @@
+require 'hypermodel/resource'
+require 'hypermodel/traverse_ancestors'
+
+module Hypermodel
+ # Public: Commands a collection of resources to build themselves in JSON-HAL
+ # format, with some links of the collection itself.
+ class Collection
+ # Public: Initializes a Collection.
+ #
+ # collection - An Array of Mongoid documents.
+ # controller - An ActionController instance.
+ #
+ # Returns nothing.
+ def initialize(collection, controller)
+ @collection = collection
+ @name = collection.first.class.name.downcase.pluralize
+ @controller = controller
+ end
+
+ # Public: Serialize the whole representation as JSON.
+ #
+ # Returns a String with the serialization.
+ def as_json(*opts)
+ links.update(embedded).as_json(*opts)
+ end
+
+ # Internal: Constructs the _links section of the response.
+ #
+ # Returns a Hash of the links of the collection. It will include, at least,
+ # a link to itself.
+ def links
+ _links = parent_link.update({
+ self: { href: @controller.polymorphic_url(collection_hierarchy) }
+ })
+
+ { _links: _links }
+ end
+
+ # Internal: Constructs the _embedded section of the response.
+ #
+ # Returns a Hash of the collection members, decorated as Resources.
+ def embedded
+ {
+ _embedded: {
+ @name => decorated_collection
+ }
+ }
+ end
+
+ #######
+ private
+ #######
+
+ # Internal: Returns a Hash with a link to the parent of the collection, if
+ # it exists, or an empty Hash otherwise.
+ def parent_link
+ link = {}
+ if collection_hierarchy.length > 1
+ parent_name = collection_hierarchy[-2].class.name.downcase
+ link[parent_name] = {
+ href: @controller.polymorphic_url(collection_hierarchy[0..-2])
+ }
+ end
+ link
+ end
+
+ # Internal: Returns a copy of the collection with its members decorated as
+ # Hypermodel Resources.
+ def decorated_collection
+ @collection.map do |element|
+ Resource.new(element, @controller)
+ end
+ end
+
+ # Internal: Returns an Array with the ancestor hierarchy of the
+ # collection, used mainly to construct URIs.
+ #
+ # The last element is always the plural name of the collection as a String.
+ #
+ # Examples
+ #
+ # collection = Hypermodel::Collection.new(post.comments)
+ # collection.send :collection_hierarchy
+ # # => [#<Blog:0x123456>, #<Post:0x456789>, "comments"]
+ #
+ def collection_hierarchy
+ @collection_hierarchy ||=
+ TraverseAncestors[@collection.first][0..-2].tap do |fields|
+ fields << @name
+ end
+ end
+ end
+end
@@ -1,4 +1,6 @@
+require 'forwardable'
require 'hypermodel/serializers/mongoid'
+require 'hypermodel/traverse_ancestors'
module Hypermodel
# Public: Responsible for building the response in JSON-HAL format. It is
@@ -13,27 +15,6 @@ class Resource
:sub_resources, :embedded_resources,
:embedding_resources
- # Public: Recursive functino that traverses a record's referential
- # hierarchy upwards.
- #
- # Returns a flattened Array with the hierarchy of records.
- TraverseUpwards = lambda do |record|
- serializer = Serializers::Mongoid.new(record)
-
- parent_name, parent_resource = (
- serializer.embedding_resources.first || serializer.resources.first
- )
-
- # If we have a parent
- if parent_resource
- # Recurse over parent hierarchies
- [TraverseUpwards[parent_resource], record].flatten
- else
- # Final case, we are the topmost parent: return ourselves
- [record]
- end
- end
-
# Public: Initializes a Resource.
#
# record - A Mongoid instance of a model.
@@ -49,9 +30,9 @@ def initialize(record, controller)
# 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)
+ # opts - Options to pass to the resource as_json.
+ def as_json(*opts)
+ attributes.update(links).update(embedded).as_json(*opts)
end
# Internal: Constructs the _links section of the response.
@@ -84,7 +65,9 @@ def polymorphic_url(record_or_hash_or_array, options = {})
{ href: @controller.polymorphic_url(record_or_hash_or_array, options = {}) }
end
+ #######
private
+ #######
# Internal: Returns a flattened array of records representing the ancestor
# chain of a given record, including itself at the end.
@@ -93,7 +76,7 @@ def polymorphic_url(record_or_hash_or_array, options = {})
#
# record - the record whose ancestor chain we'd like to retrieve.
def record_with_ancestor_chain(record)
- TraverseUpwards[record]
+ TraverseAncestors[record]
end
end
end
@@ -1,4 +1,5 @@
require 'hypermodel/resource'
+require 'hypermodel/collection'
module Hypermodel
# Public: Responsible for exposing a resource in JSON-HAL format.
@@ -28,7 +29,11 @@ def self.call(*args)
def initialize(resource_name, action, record, controller)
@resource_name = resource_name
@action = action
- @resource = Resource.new(record, controller)
+ @resource = if record.respond_to?(:each)
+ Collection.new(record, controller)
+ else
+ Resource.new(record, controller)
+ end
end
def to_json(*opts)
@@ -0,0 +1,24 @@
+require 'hypermodel/serializers/mongoid'
+
+module Hypermodel
+ # Public: Recursive function that traverses a record's referential
+ # hierarchy upwards.
+ #
+ # Returns a flattened Array with the hierarchy of records.
+ TraverseAncestors = lambda do |record|
+ serializer = Serializers::Mongoid.new(record)
+
+ parent_name, parent_resource = (
+ serializer.embedding_resources.first || serializer.resources.first
+ )
+
+ # If we have a parent
+ if parent_resource
+ # Recurse over parent hierarchies
+ [TraverseAncestors[parent_resource], record].flatten
+ else
+ # Final case, we are the topmost parent: return ourselves
+ [record]
+ end
+ end
+end
@@ -0,0 +1,56 @@
+require 'test_helper'
+require 'json'
+
+class CollectionTest < ActionController::TestCase
+ tests CommentsController
+
+ def setup
+ @blog = Blog.create(title: 'Rants')
+ @post = Post.create(
+ title: "My post",
+ body: "Foo bar baz.",
+ comments: [
+ Comment.new(body: 'Comment 1'),
+ Comment.new(body: 'Comment 2'),
+ ],
+ author: Author.create(name: 'John Smith'),
+ reviews: [
+ Review.create(body: 'This is bad'),
+ Review.create(body: 'This is good'),
+ ],
+ blog_id: @blog.id
+ )
+ @comments = @post.comments
+
+ post :index, { blog_id: @blog.id, post_id: @post.id, format: :json }
+ end
+
+ test "it returns a successful response" do
+ assert response.successful?
+ end
+
+ test "returns the self link" do
+ body = JSON.load(response.body)
+ assert_match %r{/comments}, body['_links']['self']['href']
+ end
+
+ test "returns the comments" do
+ body = JSON.load(response.body)
+
+ comments = body['_embedded']['comments']
+
+ first = @comments.first
+ last = @comments.last
+
+ assert_equal first.id.to_s, comments.first['_id']
+ assert_equal first.body, comments.first['body']
+
+ assert_equal last.id.to_s, comments.last['_id']
+ assert_equal last.body, comments.last['body']
+ end
+
+ test "returns the parent post" do
+ body = JSON.load(response.body)
+ assert_equal blog_post_url(@blog, @post), body['_links']['post']['href']
+ end
+end
@@ -0,0 +1,11 @@
+class CommentsController < ApplicationController
+ respond_to :json
+
+ def index
+ @blog = Blog.find params[:blog_id]
+ @post = @blog.posts.find params[:post_id]
+ @comments = @post.comments
+
+ respond_with(@comments, responder: Hypermodel::Responder)
+ end
+end
@@ -2,6 +2,7 @@
resources :blogs do
resources :posts do
resources :reviews
+ resources :comments
end
end
resources :authors
@@ -1,7 +1,9 @@
require 'test_helper'
require 'json'
-class PostsControllerTest < ActionController::TestCase
+class MemberTest < ActionController::TestCase
+ tests PostsController
+
def setup
@blog = Blog.create(title: 'Rants')
@post = Post.create(

0 comments on commit 52bf5c0

Please sign in to comment.