Browse files

Wow, automagic hierarchy reflection

  • Loading branch information...
1 parent c1765de commit abe6cc10d09b797f775bcdb3144db6f729dd5500 @txus txus committed May 21, 2012
View
44 README.md
@@ -40,6 +40,50 @@ Now if you ask your API for a Post:
[{"_id"=>"4fb648996b98c9091900000d", "body"=>"Comment 1"},
{"_id"=>"4fb648996b98c9091900000e", "body"=>"Comment 2"}]}}
+## Gotchas
+
+These are some implementation gotchas which are welcome to be fixed if you can
+think of workarounds :)
+
+### Routes should reflect data model
+
+For Hypermodel to generate `_links` correctly, the relationship/embedding
+structure of the model must match exactly that of the app routing. That means,
+that if you have `Blogs` that have many `Posts` which have many `Comments`,
+the routes.rb file should reflect it:
+
+````ruby
+# config/routes.rb
+resources :blogs do
+ resources :posts do
+ resources :comments
+ end
+end
+````
+
+### Every resource controller must implement the :show action at least
+
+Each resource and subresource must respond to the `show` action (so they can be
+linked from anywhere, in the `_links` section).
@oriolgual
Codegram member

This could be fixed when we introduce personalized Resources,where you can specify what to link.

@txus
Codegram member
txus added a note May 21, 2012

Yeah but no. Any resource that might appear must have a link to self, therefore, either you make sure it doesn't appear anywhere, or it will crash.

@oriolgual
Codegram member

Fuckity fuck indeed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+### The first belongs_to decides the hierarchy chain
+
+So if a `Post` belongs to an author and to a blog, and you want to access
+posts through blogs, not authors, you have to put the `belongs_to :blog`
+**before** `belongs_to :author`:
+
+````ruby
+# app/models/post.rb
+class Post
+ include Mongoid::Document
+
+ belongs_to :blog
+ belongs_to :author
+end
+````
+
+I know, lame.
+
## Contributing
* [List of hypermodel contributors][contributors]
View
43 lib/hypermodel/resource.rb
@@ -10,7 +10,29 @@ class Resource
extend Forwardable
def_delegators :@serializer, :attributes, :record, :resources,
- :sub_resources, :embedded_resources
+ :sub_resources, :embedded_resources,
+ :embedding_resources
+
+ # Public: Recursive functino that traverses a record's referential
@oriolgual
Codegram member

s/functino/function

@txus
Codegram member
txus added a note May 21, 2012

It's not a function, it's a functino, okay, learn your facts, jeez.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ # 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.
#
@@ -20,6 +42,7 @@ class Resource
# TODO: Detect record type (ActiveRecord, DataMapper, Mongoid, etc..) and
# choose the corresponding serializer.
def initialize(record, controller)
+ @record = record
@serializer = Serializers::Mongoid.new(record)
@controller = controller
end
@@ -36,14 +59,14 @@ def to_json(*opts)
# 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) }
+ _links = { self: polymorphic_url(record_with_ancestor_chain(@record)) }
resources.each do |name, resource|
- _links.update(name => polymorphic_url(resource))
+ _links.update(name => polymorphic_url(record_with_ancestor_chain(resource)))
end
sub_resources.each do |sub_resource|
- _links.update(sub_resource => polymorphic_url([record, sub_resource]))
+ _links.update(sub_resource => polymorphic_url(record_with_ancestor_chain(@record) << sub_resource))
end
{ _links: _links }
@@ -60,5 +83,17 @@ def embedded
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.
+ #
+ # It is used to generate correct polymorphic URLs.
+ #
+ # record - the record whose ancestor chain we'd like to retrieve.
+ def record_with_ancestor_chain(record)
+ TraverseUpwards[record]
+ end
end
end
View
18 lib/hypermodel/serializers/mongoid.rb
@@ -80,6 +80,18 @@ def embedded_resources
end
end
+ # Public: Returns a Hash with the resources that are embedding us (our
+ # immediate ancestors).
+ def embedding_resources
+ return {} if embedded_relations.empty?
+
+ embedded = select_embedded_by_type(::Mongoid::Relations::Embedded::In)
+
+ embedded.inject({}) do |acc, (name, _)|
+ acc.update(name => @record.send(name))
+ end
+ end
+
#######
private
#######
@@ -90,6 +102,12 @@ def select_relations_by_type(type)
end
end
+ def select_embedded_by_type(type)
+ embedded_relations.select do |name, metadata|
+ metadata.relation == type
+ end
+ end
+
def extract_embedded_attributes(name)
embedded = @record.send(name)
View
3 test/dummy/app/controllers/posts_controller.rb
@@ -2,7 +2,8 @@ class PostsController < ApplicationController
respond_to :json
def show
- @post = Post.find params[:id]
+ @blog = Blog.find params[:blog_id]
+ @post = @blog.posts.find params[:id]
respond_with(@post, responder: Hypermodel::Responder)
end
end
View
6 test/dummy/app/models/blog.rb
@@ -0,0 +1,6 @@
+class Blog
+ include Mongoid::Document
+
+ field :title
+ has_many :posts
+end
View
1 test/dummy/app/models/post.rb
@@ -5,6 +5,7 @@ class Post
field :title, type: String
field :body, type: String
+ belongs_to :blog
belongs_to :author
has_many :reviews
embeds_many :comments
View
6 test/dummy/config/routes.rb
@@ -1,6 +1,8 @@
Dummy::Application.routes.draw do
- resources :posts do
- resources :reviews
+ resources :blogs do
+ resources :posts do
+ resources :reviews
+ end
end
resources :authors
end
View
12 test/hypermodel_test.rb
@@ -3,6 +3,7 @@
class PostsControllerTest < ActionController::TestCase
def setup
+ @blog = Blog.create(title: 'Rants')
@post = Post.create(
title: "My post",
body: "Foo bar baz.",
@@ -14,10 +15,11 @@ def setup
reviews: [
Review.create(body: 'This is bad'),
Review.create(body: 'This is good'),
- ]
+ ],
+ blog_id: @blog.id
)
- post :show, { id: @post.id, format: :json }
+ post :show, { blog_id: @blog.id, id: @post.id, format: :json }
end
test "it returns a successful response" do
@@ -26,12 +28,18 @@ def setup
test "returns the post" do
body = JSON.load(response.body)
+ pp body
@oriolgual
Codegram member

Upsei!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
assert_equal @post.id.to_s, body['_id']
assert_equal 'My post', body['title']
assert_equal 'Foo bar baz.', body['body']
end
+ test "returns the parent blog" do
+ body = JSON.load(response.body)
+ assert_equal blog_url(@blog), body['_links']['blog']['href']
+ end
+
test "returns the embedded comments" do
body = JSON.load(response.body)

3 comments on commit abe6cc1

@oriolgual
Codegram member

I don't get it, why we need all this? Why blog is different from author?

@oriolgual
Codegram member

Oh, because post is a subresource of blog :P

@masylum

I thought automagic was not a cool term anymoar

Please sign in to comment.