hypermodel 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2012 YOURNAME
1
+ Copyright 2012 Codegram Technologies
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -0,0 +1,93 @@
1
+ require 'hypermodel/resource'
2
+ require 'hypermodel/traverse_ancestors'
3
+
4
+ module Hypermodel
5
+ # Public: Commands a collection of resources to build themselves in JSON-HAL
6
+ # format, with some links of the collection itself.
7
+ class Collection
8
+ # Public: Initializes a Collection.
9
+ #
10
+ # collection - An Array of Mongoid documents.
11
+ # controller - An ActionController instance.
12
+ #
13
+ # Returns nothing.
14
+ def initialize(collection, controller)
15
+ @collection = collection
16
+ @name = collection.first.class.name.downcase.pluralize
17
+ @controller = controller
18
+ end
19
+
20
+ # Public: Serialize the whole representation as JSON.
21
+ #
22
+ # Returns a String with the serialization.
23
+ def as_json(*opts)
24
+ links.update(embedded).as_json(*opts)
25
+ end
26
+
27
+ # Internal: Constructs the _links section of the response.
28
+ #
29
+ # Returns a Hash of the links of the collection. It will include, at least,
30
+ # a link to itself.
31
+ def links
32
+ _links = parent_link.update({
33
+ self: { href: @controller.polymorphic_url(collection_hierarchy) }
34
+ })
35
+
36
+ { _links: _links }
37
+ end
38
+
39
+ # Internal: Constructs the _embedded section of the response.
40
+ #
41
+ # Returns a Hash of the collection members, decorated as Resources.
42
+ def embedded
43
+ {
44
+ _embedded: {
45
+ @name => decorated_collection
46
+ }
47
+ }
48
+ end
49
+
50
+ #######
51
+ private
52
+ #######
53
+
54
+ # Internal: Returns a Hash with a link to the parent of the collection, if
55
+ # it exists, or an empty Hash otherwise.
56
+ def parent_link
57
+ link = {}
58
+ if collection_hierarchy.length > 1
59
+ parent_name = collection_hierarchy[-2].class.name.downcase
60
+ link[parent_name] = {
61
+ href: @controller.polymorphic_url(collection_hierarchy[0..-2])
62
+ }
63
+ end
64
+ link
65
+ end
66
+
67
+ # Internal: Returns a copy of the collection with its members decorated as
68
+ # Hypermodel Resources.
69
+ def decorated_collection
70
+ @collection.map do |element|
71
+ Resource.new(element, @controller)
72
+ end
73
+ end
74
+
75
+ # Internal: Returns an Array with the ancestor hierarchy of the
76
+ # collection, used mainly to construct URIs.
77
+ #
78
+ # The last element is always the plural name of the collection as a String.
79
+ #
80
+ # Examples
81
+ #
82
+ # collection = Hypermodel::Collection.new(post.comments)
83
+ # collection.send :collection_hierarchy
84
+ # # => [#<Blog:0x123456>, #<Post:0x456789>, "comments"]
85
+ #
86
+ def collection_hierarchy
87
+ @collection_hierarchy ||=
88
+ TraverseAncestors[@collection.first][0..-2].tap do |fields|
89
+ fields << @name
90
+ end
91
+ end
92
+ end
93
+ end
@@ -1,4 +1,6 @@
1
- require 'hypermodel/serializers/mongoid'
1
+ require 'forwardable'
2
+ require 'hypermodel/traverse_ancestors'
3
+ require 'hypermodel/serializer'
2
4
 
3
5
  module Hypermodel
4
6
  # Public: Responsible for building the response in JSON-HAL format. It is
@@ -13,27 +15,6 @@ module Hypermodel
13
15
  :sub_resources, :embedded_resources,
14
16
  :embedding_resources
15
17
 
16
- # Public: Recursive functino that traverses a record's referential
17
- # hierarchy upwards.
18
- #
19
- # Returns a flattened Array with the hierarchy of records.
20
- TraverseUpwards = lambda do |record|
21
- serializer = Serializers::Mongoid.new(record)
22
-
23
- parent_name, parent_resource = (
24
- serializer.embedding_resources.first || serializer.resources.first
25
- )
26
-
27
- # If we have a parent
28
- if parent_resource
29
- # Recurse over parent hierarchies
30
- [TraverseUpwards[parent_resource], record].flatten
31
- else
32
- # Final case, we are the topmost parent: return ourselves
33
- [record]
34
- end
35
- end
36
-
37
18
  # Public: Initializes a Resource.
38
19
  #
39
20
  # record - A Mongoid instance of a model.
@@ -43,15 +24,15 @@ module Hypermodel
43
24
  # choose the corresponding serializer.
44
25
  def initialize(record, controller)
45
26
  @record = record
46
- @serializer = Serializers::Mongoid.new(record)
27
+ @serializer = Serializer.build(record)
47
28
  @controller = controller
48
29
  end
49
30
 
50
31
  # Public: Returns a Hash of the resource in JSON-HAL.
51
32
  #
52
- # opts - Options to pass to the resource to_json.
53
- def to_json(*opts)
54
- attributes.update(links).update(embedded).to_json(*opts)
33
+ # opts - Options to pass to the resource as_json.
34
+ def as_json(*opts)
35
+ attributes.update(links).update(embedded).as_json(*opts)
55
36
  end
56
37
 
57
38
  # Internal: Constructs the _links section of the response.
@@ -84,7 +65,9 @@ module Hypermodel
84
65
  { href: @controller.polymorphic_url(record_or_hash_or_array, options = {}) }
85
66
  end
86
67
 
68
+ #######
87
69
  private
70
+ #######
88
71
 
89
72
  # Internal: Returns a flattened array of records representing the ancestor
90
73
  # chain of a given record, including itself at the end.
@@ -93,7 +76,7 @@ module Hypermodel
93
76
  #
94
77
  # record - the record whose ancestor chain we'd like to retrieve.
95
78
  def record_with_ancestor_chain(record)
96
- TraverseUpwards[record]
79
+ TraverseAncestors[record]
97
80
  end
98
81
  end
99
82
  end
@@ -1,4 +1,5 @@
1
1
  require 'hypermodel/resource'
2
+ require 'hypermodel/collection'
2
3
 
3
4
  module Hypermodel
4
5
  # Public: Responsible for exposing a resource in JSON-HAL format.
@@ -28,7 +29,11 @@ module Hypermodel
28
29
  def initialize(resource_name, action, record, controller)
29
30
  @resource_name = resource_name
30
31
  @action = action
31
- @resource = Resource.new(record, controller)
32
+ @resource = if record.respond_to?(:each)
33
+ Collection.new(record, controller)
34
+ else
35
+ Resource.new(record, controller)
36
+ end
32
37
  end
33
38
 
34
39
  def to_json(*opts)
@@ -0,0 +1,13 @@
1
+ require 'hypermodel/serializers/mongoid'
2
+
3
+ module Hypermodel
4
+ # Private: Responsible for instantiating the correct serializer for a given
5
+ # record. Right now only works with Mongoid.
6
+ class Serializer
7
+
8
+ # Public: Returns a matching Serializer inspecting the ORM of the record.
9
+ def self.build(record)
10
+ Serializers::Mongoid.new(record)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+ require 'hypermodel/serializers/mongoid'
2
+
3
+ module Hypermodel
4
+ # Public: Recursive function that traverses a record's referential
5
+ # hierarchy upwards.
6
+ #
7
+ # Returns a flattened Array with the hierarchy of records.
8
+ TraverseAncestors = lambda do |record|
9
+ serializer = Serializers::Mongoid.new(record)
10
+
11
+ parent_name, parent_resource = (
12
+ serializer.embedding_resources.first || serializer.resources.first
13
+ )
14
+
15
+ # If we have a parent
16
+ if parent_resource
17
+ # Recurse over parent hierarchies
18
+ [TraverseAncestors[parent_resource], record].flatten
19
+ else
20
+ # Final case, we are the topmost parent: return ourselves
21
+ [record]
22
+ end
23
+ end
24
+ end
@@ -1,3 +1,3 @@
1
1
  module Hypermodel
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -0,0 +1,56 @@
1
+ require 'test_helper'
2
+ require 'json'
3
+
4
+ class CollectionTest < ActionController::TestCase
5
+ tests CommentsController
6
+
7
+ def setup
8
+ @blog = Blog.create(title: 'Rants')
9
+ @post = Post.create(
10
+ title: "My post",
11
+ body: "Foo bar baz.",
12
+ comments: [
13
+ Comment.new(body: 'Comment 1'),
14
+ Comment.new(body: 'Comment 2'),
15
+ ],
16
+ author: Author.create(name: 'John Smith'),
17
+ reviews: [
18
+ Review.create(body: 'This is bad'),
19
+ Review.create(body: 'This is good'),
20
+ ],
21
+ blog_id: @blog.id
22
+ )
23
+ @comments = @post.comments
24
+
25
+ post :index, { blog_id: @blog.id, post_id: @post.id, format: :json }
26
+ end
27
+
28
+ test "it returns a successful response" do
29
+ assert response.successful?
30
+ end
31
+
32
+ test "returns the self link" do
33
+ body = JSON.load(response.body)
34
+ assert_match %r{/comments}, body['_links']['self']['href']
35
+ end
36
+
37
+ test "returns the comments" do
38
+ body = JSON.load(response.body)
39
+
40
+ comments = body['_embedded']['comments']
41
+
42
+ first = @comments.first
43
+ last = @comments.last
44
+
45
+ assert_equal first.id.to_s, comments.first['_id']
46
+ assert_equal first.body, comments.first['body']
47
+
48
+ assert_equal last.id.to_s, comments.last['_id']
49
+ assert_equal last.body, comments.last['body']
50
+ end
51
+
52
+ test "returns the parent post" do
53
+ body = JSON.load(response.body)
54
+ assert_equal blog_post_url(@blog, @post), body['_links']['post']['href']
55
+ end
56
+ end
@@ -0,0 +1,11 @@
1
+ class CommentsController < ApplicationController
2
+ respond_to :json
3
+
4
+ def index
5
+ @blog = Blog.find params[:blog_id]
6
+ @post = @blog.posts.find params[:post_id]
7
+ @comments = @post.comments
8
+
9
+ respond_with(@comments, responder: Hypermodel::Responder)
10
+ end
11
+ end
@@ -2,6 +2,7 @@ Dummy::Application.routes.draw do
2
2
  resources :blogs do
3
3
  resources :posts do
4
4
  resources :reviews
5
+ resources :comments
5
6
  end
6
7
  end
7
8
  resources :authors