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 +1 -1
- data/lib/hypermodel/collection.rb +93 -0
- data/lib/hypermodel/resource.rb +10 -27
- data/lib/hypermodel/responder.rb +6 -1
- data/lib/hypermodel/serializer.rb +13 -0
- data/lib/hypermodel/traverse_ancestors.rb +24 -0
- data/lib/hypermodel/version.rb +1 -1
- data/test/collection_test.rb +56 -0
- data/test/dummy/app/controllers/comments_controller.rb +11 -0
- data/test/dummy/config/routes.rb +1 -0
- data/test/dummy/log/test.log +5396 -0
- data/test/{hypermodel_test.rb → member_test.rb} +3 -2
- metadata +13 -6
data/MIT-LICENSE
CHANGED
@@ -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
|
data/lib/hypermodel/resource.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
require '
|
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 =
|
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
|
53
|
-
def
|
54
|
-
attributes.update(links).update(embedded).
|
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
|
-
|
79
|
+
TraverseAncestors[record]
|
97
80
|
end
|
98
81
|
end
|
99
82
|
end
|
data/lib/hypermodel/responder.rb
CHANGED
@@ -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 =
|
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
|
data/lib/hypermodel/version.rb
CHANGED
@@ -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
|