├── test
├── dummy
│ ├── log
│ │ └── .gitkeep
│ ├── app
│ │ ├── mailers
│ │ │ └── .gitkeep
│ │ ├── models
│ │ │ ├── .gitkeep
│ │ │ ├── blog.rb
│ │ │ ├── author.rb
│ │ │ ├── review.rb
│ │ │ ├── comment.rb
│ │ │ └── post.rb
│ │ ├── helpers
│ │ │ └── application_helper.rb
│ │ ├── controllers
│ │ │ ├── application_controller.rb
│ │ │ ├── posts_controller.rb
│ │ │ └── comments_controller.rb
│ │ ├── views
│ │ │ └── layouts
│ │ │ │ └── application.html.erb
│ │ └── assets
│ │ │ ├── stylesheets
│ │ │ └── application.css
│ │ │ └── javascripts
│ │ │ └── application.js
│ ├── lib
│ │ └── assets
│ │ │ └── .gitkeep
│ ├── public
│ │ ├── favicon.ico
│ │ ├── 500.html
│ │ ├── 422.html
│ │ └── 404.html
│ ├── config.ru
│ ├── config
│ │ ├── environment.rb
│ │ ├── routes.rb
│ │ ├── locales
│ │ │ └── en.yml
│ │ ├── initializers
│ │ │ ├── mime_types.rb
│ │ │ ├── wrap_parameters.rb
│ │ │ ├── backtrace_silencers.rb
│ │ │ ├── session_store.rb
│ │ │ ├── secret_token.rb
│ │ │ └── inflections.rb
│ │ ├── boot.rb
│ │ ├── mongoid.yml
│ │ ├── environments
│ │ │ ├── development.rb
│ │ │ ├── test.rb
│ │ │ └── production.rb
│ │ └── application.rb
│ ├── Rakefile
│ ├── script
│ │ └── rails
│ └── README.rdoc
├── test_helper.rb
├── member_test.rb
└── collection_test.rb
├── .rvmrc
├── lib
├── hypermodel
│ ├── version.rb
│ ├── serializer.rb
│ ├── traverse_ancestors.rb
│ ├── responder.rb
│ ├── empty_collection.rb
│ ├── resource.rb
│ ├── collection.rb
│ └── serializers
│ │ └── mongoid.rb
├── tasks
│ └── hypermodel_tasks.rake
└── hypermodel.rb
├── .travis.yml
├── .yardopts
├── .gitignore
├── Rakefile
├── Gemfile
├── hypermodel.gemspec
├── MIT-LICENSE
├── Gemfile.lock
└── README.md
/test/dummy/log/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/mailers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/models/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/lib/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.rvmrc:
--------------------------------------------------------------------------------
1 | rvm --create use 1.9.3@hypermodel
2 |
--------------------------------------------------------------------------------
/lib/hypermodel/version.rb:
--------------------------------------------------------------------------------
1 | module Hypermodel
2 | VERSION = "0.2.1"
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | env:
2 | - RBXOPT=-X19 JRUBY_OPTS="--1.9"
3 | rvm:
4 | - 1.9.2
5 | - 1.9.3
6 | - rbx
7 | - jruby
8 |
--------------------------------------------------------------------------------
/test/dummy/app/models/blog.rb:
--------------------------------------------------------------------------------
1 | class Blog
2 | include Mongoid::Document
3 |
4 | field :title
5 | has_many :posts
6 | end
7 |
--------------------------------------------------------------------------------
/.yardopts:
--------------------------------------------------------------------------------
1 | --title "Hypermodel"
2 | --readme README.md
3 | --protected
4 | --private
5 | --plugin tomdoc
6 | lib
7 | -
8 | [A-Z]*.*
9 |
--------------------------------------------------------------------------------
/lib/tasks/hypermodel_tasks.rake:
--------------------------------------------------------------------------------
1 | # desc "Explaining what the task does"
2 | # task :hypermodel do
3 | # # Task goes here
4 | # end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery
3 | end
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .bundle/
2 | log/*.log
3 | pkg/
4 | test/dummy/db/*.sqlite3
5 | test/dummy/log/*.log
6 | test/dummy/tmp/
7 | test/dummy/.sass-cache
8 | doc/
9 | .yardoc/
10 |
--------------------------------------------------------------------------------
/test/dummy/app/models/author.rb:
--------------------------------------------------------------------------------
1 | class Author
2 | include Mongoid::Document
3 | include Mongoid::Timestamps
4 |
5 | field :name, type: String
6 |
7 | has_many :posts
8 | end
9 |
--------------------------------------------------------------------------------
/test/dummy/app/models/review.rb:
--------------------------------------------------------------------------------
1 | class Review
2 | include Mongoid::Document
3 | include Mongoid::Timestamps
4 |
5 | field :body, type: String
6 |
7 | belongs_to :post
8 | end
9 |
--------------------------------------------------------------------------------
/test/dummy/app/models/comment.rb:
--------------------------------------------------------------------------------
1 | class Comment
2 | include Mongoid::Document
3 | include Mongoid::Timestamps
4 |
5 | field :body, type: String
6 |
7 | embedded_in :post
8 | end
9 |
--------------------------------------------------------------------------------
/test/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Dummy::Application
5 |
--------------------------------------------------------------------------------
/test/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the rails application
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the rails application
5 | Dummy::Application.initialize!
6 |
--------------------------------------------------------------------------------
/test/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.routes.draw do
2 | resources :blogs do
3 | resources :posts do
4 | resources :reviews
5 | resources :comments
6 | end
7 | end
8 | resources :authors
9 | end
10 |
--------------------------------------------------------------------------------
/lib/hypermodel.rb:
--------------------------------------------------------------------------------
1 | # Public: A Hypermodel is a representation of a resource in a JSON-HAL format.
2 | # To learn more about JSON HAL see http://stateless.co/hal_specification.html
3 | module Hypermodel
4 | end
5 |
6 | require 'hypermodel/responder'
7 |
--------------------------------------------------------------------------------
/test/dummy/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Sample localization file for English. Add more files in this directory for other locales.
2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 |
4 | en:
5 | hello: "Hello world"
6 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 | # Mime::Type.register_alias "text/html", :iphone
6 |
--------------------------------------------------------------------------------
/test/dummy/app/models/post.rb:
--------------------------------------------------------------------------------
1 | class Post
2 | include Mongoid::Document
3 | include Mongoid::Timestamps
4 |
5 | field :title, type: String
6 | field :body, type: String
7 |
8 | belongs_to :blog
9 | belongs_to :author
10 | has_many :reviews
11 | embeds_many :comments
12 | end
13 |
--------------------------------------------------------------------------------
/test/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | gemfile = File.expand_path('../../../../Gemfile', __FILE__)
3 |
4 | if File.exist?(gemfile)
5 | ENV['BUNDLE_GEMFILE'] = gemfile
6 | require 'bundler'
7 | Bundler.setup
8 | end
9 |
10 | $:.unshift File.expand_path('../../../../lib', __FILE__)
--------------------------------------------------------------------------------
/test/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | # Add your own tasks in files placed in lib/tasks ending in .rake,
3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
4 |
5 | require File.expand_path('../config/application', __FILE__)
6 |
7 | Dummy::Application.load_tasks
8 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/posts_controller.rb:
--------------------------------------------------------------------------------
1 | class PostsController < ApplicationController
2 | respond_to :json
3 |
4 | def show
5 | @blog = Blog.find params[:blog_id]
6 | @post = @blog.posts.find params[:id]
7 | respond_with(@post, responder: Hypermodel::Responder)
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/dummy/script/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3 |
4 | APP_PATH = File.expand_path('../../config/application', __FILE__)
5 | require File.expand_path('../../config/boot', __FILE__)
6 | require 'rails/commands'
7 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 | <%= stylesheet_link_tag "application", :media => "all" %>
6 | <%= javascript_include_tag "application" %>
7 | <%= csrf_meta_tags %>
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/comments_controller.rb:
--------------------------------------------------------------------------------
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
12 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | #
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 |
--------------------------------------------------------------------------------
/lib/hypermodel/serializer.rb:
--------------------------------------------------------------------------------
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
14 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session'
4 |
5 | # Use the database for sessions instead of the cookie-based default,
6 | # which shouldn't be used to store highly confidential information
7 | # (create the session table with "rails generate session_migration")
8 | # Dummy::Application.config.session_store :active_record_store
9 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # Configure Rails Environment
2 | ENV["RAILS_ENV"] = "test"
3 |
4 | require File.expand_path("../dummy/config/environment.rb", __FILE__)
5 | require "rails/test_help"
6 |
7 | Rails.backtrace_cleaner.remove_silencers!
8 |
9 | # Load support files
10 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
11 |
12 | # Load fixtures from the engine
13 | if ActiveSupport::TestCase.method_defined?(:fixture_path=)
14 | ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__)
15 | end
16 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 | # Make sure the secret is at least 30 characters and all random,
6 | # no regular words or you'll be exposed to dictionary attacks.
7 | Dummy::Application.config.secret_token = '6998c8cf79f3381ce913f5f912d58e721279d5eb17211b402f5ac308cd2874823cdd8291015036d2b37c009bd23c0e87f23d0bf0d70eecc564f59479888b28f9'
8 |
--------------------------------------------------------------------------------
/test/dummy/config/mongoid.yml:
--------------------------------------------------------------------------------
1 | development:
2 | host: localhost
3 | database: hypermodel_development
4 |
5 | test:
6 | host: localhost
7 | database: hypermodel_test
8 |
9 | # set these environment variables on your prod server
10 | production:
11 | host: <%= ENV['MONGOID_HOST'] %>
12 | port: <%= ENV['MONGOID_PORT'] %>
13 | username: <%= ENV['MONGOID_USERNAME'] %>
14 | password: <%= ENV['MONGOID_PASSWORD'] %>
15 | database: <%= ENV['MONGOID_DATABASE'] %>
16 | # slaves:
17 | # - host: slave1.local
18 | # port: 27018
19 | # - host: slave2.local
20 | # port: 27019
21 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format
4 | # (all these examples are active by default):
5 | # ActiveSupport::Inflector.inflections do |inflect|
6 | # inflect.plural /^(ox)$/i, '\1en'
7 | # inflect.singular /^(ox)en/i, '\1'
8 | # inflect.irregular 'person', 'people'
9 | # inflect.uncountable %w( fish sheep )
10 | # end
11 | #
12 | # These inflection rules are supported but not enabled by default:
13 | # ActiveSupport::Inflector.inflections do |inflect|
14 | # inflect.acronym 'RESTful'
15 | # end
16 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the top of the
9 | * compiled file, but it's generally better to create a new file per style scope.
10 | *
11 | *= require_self
12 | *= require_tree .
13 | */
14 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | begin
3 | require 'bundler/setup'
4 | rescue LoadError
5 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6 | end
7 |
8 | require 'yard'
9 | YARD::Config.load_plugin('yard-tomdoc')
10 | YARD::Rake::YardocTask.new do |t|
11 | t.files = ['lib/**/*.rb']
12 | t.options = %w(-r README.md)
13 | end
14 |
15 |
16 | Bundler::GemHelper.install_tasks
17 |
18 | require 'rake/testtask'
19 |
20 | Rake::TestTask.new(:test) do |t|
21 | t.libs << 'lib'
22 | t.libs << 'test'
23 | t.pattern = 'test/**/*_test.rb'
24 | t.verbose = false
25 | end
26 |
27 |
28 | task :default => :test
29 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // the compiled file.
9 | //
10 | // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
11 | // GO AFTER THE REQUIRES BELOW.
12 | //
13 | //= require_tree .
14 |
--------------------------------------------------------------------------------
/test/dummy/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
We're sorry, but something went wrong.
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/dummy/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The change you wanted was rejected.
23 |
Maybe you tried to change something you didn't have access to.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Declare your gem's dependencies in hypermodel.gemspec.
4 | # Bundler will treat runtime dependencies like base dependencies, and
5 | # development dependencies will be added by default to the :development group.
6 | gemspec
7 |
8 | # jquery-rails is used by the dummy application
9 | gem "jquery-rails"
10 | gem "bson_ext"
11 |
12 | gem 'redcarpet'
13 | gem 'yard', '~> 0.7.5'
14 | gem 'yard-tomdoc', git: 'git://github.com/rubyworks/yard-tomdoc'
15 |
16 | # Declare any dependencies that are still in development here instead of in
17 | # your gemspec. These might include edge Rails or gems from your path or
18 | # Git. Remember to move these dependencies to your gemspec before releasing
19 | # your gem to rubygems.org.
20 |
21 | # To use debugger
22 | # gem 'ruby-debug19', :require => 'ruby-debug'
23 |
--------------------------------------------------------------------------------
/lib/hypermodel/traverse_ancestors.rb:
--------------------------------------------------------------------------------
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
25 |
--------------------------------------------------------------------------------
/test/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The page you were looking for doesn't exist.
23 |
You may have mistyped the address or the page may have moved.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/hypermodel.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path("../lib", __FILE__)
2 |
3 | # Maintain your gem's version:
4 | require "hypermodel/version"
5 |
6 | # Describe your gem and declare its dependencies:
7 | Gem::Specification.new do |s|
8 | s.name = "hypermodel"
9 | s.version = Hypermodel::VERSION
10 | s.authors = ["Josep M. Bach"]
11 | s.email = ["josep.m.bach@gmail.com"]
12 | s.homepage = "https://github.com/codegram/hypermodel"
13 | s.summary = "Rails Responder to generate an automagic JSON HAL representation for your Mongoid models"
14 | s.description = "Rails Responder to generate an automagic JSON HAL representation for your Mongoid models"
15 |
16 | s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.md"]
17 | s.test_files = Dir["test/**/*"]
18 |
19 | s.add_dependency "rails", "~> 3.2.3"
20 | s.add_dependency "mongoid"
21 |
22 | s.add_development_dependency "sqlite3"
23 | end
24 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2012 Codegram Technologies
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Log error messages when you accidentally call methods on nil.
10 | config.whiny_nils = true
11 |
12 | # Show full error reports and disable caching
13 | config.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 |
16 | # Don't care if the mailer can't send
17 | # config.action_mailer.raise_delivery_errors = false
18 |
19 | # Print deprecation notices to the Rails logger
20 | config.active_support.deprecation = :log
21 |
22 | # Only use best-standards-support built into browsers
23 | config.action_dispatch.best_standards_support = :builtin
24 |
25 |
26 | # Do not compress assets
27 | config.assets.compress = false
28 |
29 | # Expands the lines which load the assets
30 | config.assets.debug = true
31 | end
32 |
--------------------------------------------------------------------------------
/lib/hypermodel/responder.rb:
--------------------------------------------------------------------------------
1 | require 'hypermodel/resource'
2 | require 'hypermodel/collection'
3 | require 'hypermodel/empty_collection'
4 |
5 | module Hypermodel
6 | # Public: Responsible for exposing a resource in JSON-HAL format.
7 | #
8 | # Examples
9 | #
10 | # class PostsController < ApplicationController
11 | # respond_to :json
12 | #
13 | # def show
14 | # @post = Post.find params[:id]
15 | # respond_with(@post, responder: Hypermodel::Responder)
16 | # end
17 | # end
18 | class Responder
19 | def self.call(*args)
20 | controller = args[0]
21 | resource = args[1].first
22 | resource_name = controller.params["controller"]
23 | action = controller.params["action"]
24 |
25 | responder = new resource_name, action, resource, controller
26 |
27 | controller.render json: responder
28 | end
29 |
30 | def initialize(resource_name, action, record, controller)
31 | @resource_name = resource_name
32 | @action = action
33 |
34 | if record.respond_to?(:each)
35 | @resource = if record.empty?
36 | EmptyCollection.new(resource_name, controller)
37 | else
38 | Collection.new(record, controller)
39 | end
40 | else
41 | @resource = Resource.new(record, controller)
42 | end
43 | end
44 |
45 | def to_json(*opts)
46 | @resource.to_json(*opts)
47 | end
48 | end
49 | end
50 |
51 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Configure static asset server for tests with Cache-Control for performance
11 | config.serve_static_assets = true
12 | config.static_cache_control = "public, max-age=3600"
13 |
14 | # Log error messages when you accidentally call methods on nil
15 | config.whiny_nils = true
16 |
17 | # Show full error reports and disable caching
18 | config.consider_all_requests_local = true
19 | config.action_controller.perform_caching = false
20 |
21 | # Raise exceptions instead of rendering exception templates
22 | config.action_dispatch.show_exceptions = false
23 |
24 | # Disable request forgery protection in test environment
25 | config.action_controller.allow_forgery_protection = false
26 |
27 | # Tell Action Mailer not to deliver emails to the real world.
28 | # The :test delivery method accumulates sent emails in the
29 | # ActionMailer::Base.deliveries array.
30 | # config.action_mailer.delivery_method = :test
31 |
32 |
33 | # Print deprecation notices to the stderr
34 | config.active_support.deprecation = :stderr
35 | end
36 |
--------------------------------------------------------------------------------
/lib/hypermodel/empty_collection.rb:
--------------------------------------------------------------------------------
1 | require 'hypermodel/resource'
2 |
3 | module Hypermodel
4 | # Public: Represents an empty Collection.
5 | class EmptyCollection
6 | # Public: Initializes a Collection.
7 | #
8 | # collection - An Array of Mongoid documents.
9 | # controller - An ActionController instance.
10 | #
11 | # Returns nothing.
12 | def initialize(resource_name, controller)
13 | @name = resource_name
14 | @controller = controller
15 | end
16 |
17 | # Public: Serialize the whole representation as JSON.
18 | #
19 | # Returns a String with the serialization.
20 | def as_json(*opts)
21 | links.update(embedded).as_json(*opts)
22 | end
23 |
24 | # Internal: Constructs the _links section of the response.
25 | #
26 | # Returns a Hash of the links of the collection. It will include, at least,
27 | # a link to itself.
28 | def links
29 | url = @controller.request.url
30 | url = url.split('.')[0..-2].join('.') if url =~ /\.\w+$/
31 |
32 | parent_url = url.split('/')[0..-2].join('/')
33 | parent_name = parent_url.split('/')[0..-2].last.singularize
34 |
35 | _links = {
36 | self: { href: url },
37 | parent_name => { href: parent_url }
38 | }
39 |
40 | { _links: _links }
41 | end
42 |
43 | # Internal: Constructs an empty _embedded section of the response.
44 | #
45 | # Returns an empty collection.
46 | def embedded
47 | { _embedded: { @name => [] } }
48 | end
49 | end
50 | end
51 |
52 |
--------------------------------------------------------------------------------
/test/member_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'json'
3 |
4 | class MemberTest < ActionController::TestCase
5 | tests PostsController
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 |
24 | post :show, { blog_id: @blog.id, id: @post.id, format: :json }
25 | end
26 |
27 | test "it returns a successful response" do
28 | assert response.successful?
29 | end
30 |
31 | test "returns the post" do
32 | body = JSON.load(response.body)
33 |
34 | assert_equal @post.id.to_s, body['_id']
35 | assert_equal 'My post', body['title']
36 | assert_equal 'Foo bar baz.', body['body']
37 | end
38 |
39 | test "returns the parent blog" do
40 | body = JSON.load(response.body)
41 | assert_equal blog_url(@blog), body['_links']['blog']['href']
42 | end
43 |
44 | test "returns the embedded comments" do
45 | body = JSON.load(response.body)
46 |
47 | assert_nil body['comments']
48 | comments = body['_embedded']['comments']
49 |
50 | assert_equal 'Comment 1', comments.first['body']
51 | assert_equal 'Comment 2', comments.last['body']
52 | end
53 |
54 | test "returns the self link" do
55 | body = JSON.load(response.body)
56 | assert_match /\/posts\/#{@post.id}/, body['_links']['self']['href']
57 | end
58 |
59 | test "returns the parent author" do
60 | body = JSON.load(response.body)
61 |
62 | assert_nil body['author']
63 | assert_match /\/authors\/#{@post.author.id}/, body['_links']['author']['href']
64 | end
65 |
66 | test "returns the links to its reviews" do
67 | body = JSON.load(response.body)
68 |
69 | assert_nil body['reviews']
70 | assert_match %r{/posts/#{@post.id}/reviews}, body['_links']['reviews']['href']
71 | end
72 | end
--------------------------------------------------------------------------------
/test/collection_test.rb:
--------------------------------------------------------------------------------
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 | end
25 |
26 | def request!
27 | post :index, { blog_id: @blog.id, post_id: @post.id, format: :json }
28 | end
29 |
30 | test "it returns a successful response" do
31 | request!
32 | assert response.successful?
33 | end
34 |
35 | test "returns the self link" do
36 | request!
37 | body = JSON.load(response.body)
38 | assert_match %r{/comments}, body['_links']['self']['href']
39 | end
40 |
41 | test "returns the comments" do
42 | request!
43 | body = JSON.load(response.body)
44 |
45 | comments = body['_embedded']['comments']
46 |
47 | first = @comments.first
48 | last = @comments.last
49 |
50 | assert_equal first.id.to_s, comments.first['_id']
51 | assert_equal first.body, comments.first['body']
52 |
53 | assert_equal last.id.to_s, comments.last['_id']
54 | assert_equal last.body, comments.last['body']
55 | end
56 |
57 | test "returns the parent post" do
58 | request!
59 | body = JSON.load(response.body)
60 | assert_equal blog_post_url(@blog, @post), body['_links']['post']['href']
61 | end
62 |
63 | test "it handles empty collections gracefully" do
64 | blog = Blog.create(title: 'Hey')
65 | _post = blog.posts.create(title: 'hey') # Post with no comments
66 |
67 | post :index, { blog_id: blog.id, post_id: _post.id, format: :json }
68 | body = JSON.load(response.body)
69 |
70 | assert response.successful?
71 | assert_equal [], body['_embedded']['comments']
72 |
73 | assert_equal blog_post_comments_url(blog.id, _post.id), body['_links']['self']['href']
74 | assert_equal blog_post_url(blog.id, _post.id), body['_links']['post']['href']
75 | end
76 | end
--------------------------------------------------------------------------------
/test/dummy/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # Code is not reloaded between requests
5 | config.cache_classes = true
6 |
7 | # Full error reports are disabled and caching is turned on
8 | config.consider_all_requests_local = false
9 | config.action_controller.perform_caching = true
10 |
11 | # Disable Rails's static asset server (Apache or nginx will already do this)
12 | config.serve_static_assets = false
13 |
14 | # Compress JavaScripts and CSS
15 | config.assets.compress = true
16 |
17 | # Don't fallback to assets pipeline if a precompiled asset is missed
18 | config.assets.compile = false
19 |
20 | # Generate digests for assets URLs
21 | config.assets.digest = true
22 |
23 | # Defaults to Rails.root.join("public/assets")
24 | # config.assets.manifest = YOUR_PATH
25 |
26 | # Specifies the header that your server uses for sending files
27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
29 |
30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
31 | # config.force_ssl = true
32 |
33 | # See everything in the log (default is :info)
34 | # config.log_level = :debug
35 |
36 | # Prepend all log lines with the following tags
37 | # config.log_tags = [ :subdomain, :uuid ]
38 |
39 | # Use a different logger for distributed setups
40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
41 |
42 | # Use a different cache store in production
43 | # config.cache_store = :mem_cache_store
44 |
45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server
46 | # config.action_controller.asset_host = "http://assets.example.com"
47 |
48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
49 | # config.assets.precompile += %w( search.js )
50 |
51 | # Disable delivery errors, bad email addresses will be ignored
52 | # config.action_mailer.raise_delivery_errors = false
53 |
54 | # Enable threaded mode
55 | # config.threadsafe!
56 |
57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
58 | # the I18n.default_locale when a translation can not be found)
59 | config.i18n.fallbacks = true
60 |
61 | # Send deprecation notices to registered listeners
62 | config.active_support.deprecation = :notify
63 |
64 | end
65 |
--------------------------------------------------------------------------------
/test/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | # Pick the frameworks you want:
4 | # require "active_record/railtie"
5 |
6 | require "action_controller/railtie"
7 | # require "action_mailer/railtie"
8 | # require "active_resource/railtie"
9 | # require "sprockets/railtie"
10 | require "rails/test_unit/railtie"
11 |
12 | Bundler.require
13 | require "hypermodel"
14 | require "mongoid"
15 |
16 | module Dummy
17 | class Application < Rails::Application
18 | # Settings in config/environments/* take precedence over those specified here.
19 | # Application configuration should go into files in config/initializers
20 | # -- all .rb files in that directory are automatically loaded.
21 |
22 | # Custom directories with classes and modules you want to be autoloadable.
23 | # config.autoload_paths += %W(#{config.root}/extras)
24 |
25 | # Only load the plugins named here, in the order given (default is alphabetical).
26 | # :all can be used as a placeholder for all plugins not explicitly named.
27 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
28 |
29 | # Activate observers that should always be running.
30 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
31 |
32 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
33 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
34 | # config.time_zone = 'Central Time (US & Canada)'
35 |
36 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
37 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
38 | # config.i18n.default_locale = :de
39 |
40 | # Configure the default encoding used in templates for Ruby 1.9.
41 | config.encoding = "utf-8"
42 |
43 | # Configure sensitive parameters which will be filtered from the log file.
44 | config.filter_parameters += [:password]
45 |
46 | # Use SQL instead of Active Record's schema dumper when creating the database.
47 | # This is necessary if your schema can't be completely dumped by the schema dumper,
48 | # like if you have constraints or database-specific column types
49 | # config.active_record.schema_format = :sql
50 |
51 | # Enforce whitelist mode for mass assignment.
52 | # This will create an empty whitelist of attributes available for mass-assignment for all models
53 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible
54 | # parameters by using an attr_accessible or attr_protected declaration.
55 | # config.active_record.whitelist_attributes = true
56 |
57 | # Enable the asset pipeline
58 | config.assets.enabled = true
59 |
60 | # Version of your assets, change this if you want to expire all your assets
61 | config.assets.version = '1.0'
62 | end
63 | end
64 |
65 |
--------------------------------------------------------------------------------
/lib/hypermodel/resource.rb:
--------------------------------------------------------------------------------
1 | require 'forwardable'
2 | require 'hypermodel/traverse_ancestors'
3 | require 'hypermodel/serializer'
4 |
5 | module Hypermodel
6 | # Public: Responsible for building the response in JSON-HAL format. It is
7 | # meant to be used by Hypermodel::Responder.
8 | #
9 | # In future versions one will be able to subclass it and personalize a
10 | # Resource for each diffent model, i.e. creating a PostResource.
11 | class Resource
12 | extend Forwardable
13 |
14 | def_delegators :@serializer, :attributes, :record, :resources,
15 | :sub_resources, :embedded_resources,
16 | :embedding_resources
17 |
18 | # Public: Initializes a Resource.
19 | #
20 | # record - A Mongoid instance of a model.
21 | # controller - An ActionController instance.
22 | #
23 | # TODO: Detect record type (ActiveRecord, DataMapper, Mongoid, etc..) and
24 | # choose the corresponding serializer.
25 | def initialize(record, controller)
26 | @record = record
27 | @serializer = Serializer.build(record)
28 | @controller = controller
29 | end
30 |
31 | # Public: Returns a Hash of the resource in JSON-HAL.
32 | #
33 | # opts - Options to pass to the resource as_json.
34 | def as_json(*opts)
35 | attributes.update(links).update(embedded).as_json(*opts)
36 | end
37 |
38 | # Internal: Constructs the _links section of the response.
39 | #
40 | # Returns a Hash of the links of the resource. It will include, at least,
41 | # a link to itself.
42 | def links
43 | _links = { self: polymorphic_url(record_with_ancestor_chain(@record)) }
44 |
45 | resources.each do |name, resource|
46 | _links.update(name => polymorphic_url(record_with_ancestor_chain(resource)))
47 | end
48 |
49 | sub_resources.each do |sub_resource|
50 | _links.update(sub_resource => polymorphic_url(record_with_ancestor_chain(@record) << sub_resource))
51 | end
52 |
53 | { _links: _links }
54 | end
55 |
56 | # Internal: Constructs the _embedded section of the response.
57 | #
58 | # Returns a Hash of the embedded resources of the resource.
59 | def embedded
60 | { _embedded: embedded_resources }
61 | end
62 |
63 | # Internal: Returns the url wrapped in a Hash in HAL format.
64 | def polymorphic_url(record_or_hash_or_array, options = {})
65 | { href: @controller.polymorphic_url(record_or_hash_or_array, options = {}) }
66 | end
67 |
68 | #######
69 | private
70 | #######
71 |
72 | # Internal: Returns a flattened array of records representing the ancestor
73 | # chain of a given record, including itself at the end.
74 | #
75 | # It is used to generate correct polymorphic URLs.
76 | #
77 | # record - the record whose ancestor chain we'd like to retrieve.
78 | def record_with_ancestor_chain(record)
79 | TraverseAncestors[record]
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/lib/hypermodel/collection.rb:
--------------------------------------------------------------------------------
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 | # # => [#, #, "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
94 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: git://github.com/rubyworks/yard-tomdoc
3 | revision: caee83fb8b068fef81068e00dc8d1245354536f1
4 | specs:
5 | yard-tomdoc (0.5.0)
6 | tomparse
7 | yard
8 |
9 | PATH
10 | remote: .
11 | specs:
12 | hypermodel (0.2.1)
13 | mongoid
14 | rails (~> 3.2.3)
15 |
16 | GEM
17 | remote: http://rubygems.org/
18 | specs:
19 | actionmailer (3.2.3)
20 | actionpack (= 3.2.3)
21 | mail (~> 2.4.4)
22 | actionpack (3.2.3)
23 | activemodel (= 3.2.3)
24 | activesupport (= 3.2.3)
25 | builder (~> 3.0.0)
26 | erubis (~> 2.7.0)
27 | journey (~> 1.0.1)
28 | rack (~> 1.4.0)
29 | rack-cache (~> 1.2)
30 | rack-test (~> 0.6.1)
31 | sprockets (~> 2.1.2)
32 | activemodel (3.2.3)
33 | activesupport (= 3.2.3)
34 | builder (~> 3.0.0)
35 | activerecord (3.2.3)
36 | activemodel (= 3.2.3)
37 | activesupport (= 3.2.3)
38 | arel (~> 3.0.2)
39 | tzinfo (~> 0.3.29)
40 | activeresource (3.2.3)
41 | activemodel (= 3.2.3)
42 | activesupport (= 3.2.3)
43 | activesupport (3.2.3)
44 | i18n (~> 0.6)
45 | multi_json (~> 1.0)
46 | arel (3.0.2)
47 | bson (1.6.2)
48 | bson_ext (1.6.2)
49 | bson (~> 1.6.2)
50 | builder (3.0.0)
51 | erubis (2.7.0)
52 | hike (1.2.1)
53 | i18n (0.6.0)
54 | journey (1.0.3)
55 | jquery-rails (2.0.2)
56 | railties (>= 3.2.0, < 5.0)
57 | thor (~> 0.14)
58 | json (1.7.3)
59 | mail (2.4.4)
60 | i18n (>= 0.4.0)
61 | mime-types (~> 1.16)
62 | treetop (~> 1.4.8)
63 | mime-types (1.18)
64 | mongo (1.6.2)
65 | bson (~> 1.6.2)
66 | mongoid (2.4.10)
67 | activemodel (~> 3.1)
68 | mongo (~> 1.3)
69 | tzinfo (~> 0.3.22)
70 | multi_json (1.3.5)
71 | polyglot (0.3.3)
72 | rack (1.4.1)
73 | rack-cache (1.2)
74 | rack (>= 0.4)
75 | rack-ssl (1.3.2)
76 | rack
77 | rack-test (0.6.1)
78 | rack (>= 1.0)
79 | rails (3.2.3)
80 | actionmailer (= 3.2.3)
81 | actionpack (= 3.2.3)
82 | activerecord (= 3.2.3)
83 | activeresource (= 3.2.3)
84 | activesupport (= 3.2.3)
85 | bundler (~> 1.0)
86 | railties (= 3.2.3)
87 | railties (3.2.3)
88 | actionpack (= 3.2.3)
89 | activesupport (= 3.2.3)
90 | rack-ssl (~> 1.3.2)
91 | rake (>= 0.8.7)
92 | rdoc (~> 3.4)
93 | thor (~> 0.14.6)
94 | rake (0.9.2.2)
95 | rdoc (3.12)
96 | json (~> 1.4)
97 | redcarpet (2.1.1)
98 | sprockets (2.1.3)
99 | hike (~> 1.2)
100 | rack (~> 1.0)
101 | tilt (~> 1.1, != 1.3.0)
102 | sqlite3 (1.3.6)
103 | thor (0.14.6)
104 | tilt (1.3.3)
105 | tomparse (0.2.1)
106 | treetop (1.4.10)
107 | polyglot
108 | polyglot (>= 0.3.1)
109 | tzinfo (0.3.33)
110 | yard (0.7.5)
111 |
112 | PLATFORMS
113 | ruby
114 |
115 | DEPENDENCIES
116 | bson_ext
117 | hypermodel!
118 | jquery-rails
119 | redcarpet
120 | sqlite3
121 | yard (~> 0.7.5)
122 | yard-tomdoc!
123 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # hypermodel [](http://travis-ci.org/codegram/hypermodel) [](http://gemnasium.com/codegram/hypermodel)
2 |
3 | A Rails Responder that renders any [Mongoid][mongoid] model to a [JSON HAL
4 | format][hal], suitable for Hypermedia APIs.
5 |
6 | ## Install
7 |
8 | Put this in your Gemfile:
9 |
10 | gem 'hypermodel'
11 |
12 | ## Usage
13 |
14 | ````ruby
15 | class PostsController < ApplicationController
16 | respond_to :json
17 |
18 | def show
19 | @post = Post.find params[:id]
20 | respond_with(@post, responder: Hypermodel::Responder)
21 | end
22 | end
23 | ````
24 |
25 | Now if you ask your API for a Post:
26 |
27 | {"_id"=>"4fb648996b98c90919000012",
28 | "title"=>"My post",
29 | "body"=>"Foo bar baz.",
30 | "author_id"=>"4fb648996b98c9091900000f",
31 | "updated_at"=>"2012-05-18T13:03:21Z",
32 | "created_at"=>"2012-05-18T13:03:21Z",
33 | "_links"=>
34 | {"self"=>{"href"=>"http://test.host/posts/4fb648996b98c90919000012"},
35 | "author"=>{"href"=>"http://test.host/authors/4fb648996b98c9091900000f"},
36 | "reviews"=>
37 | {"href"=>"http://test.host/posts/4fb648996b98c90919000012/reviews"}},
38 | "_embedded"=>
39 | {"comments"=>
40 | [{"_id"=>"4fb648996b98c9091900000d", "body"=>"Comment 1"},
41 | {"_id"=>"4fb648996b98c9091900000e", "body"=>"Comment 2"}]}}
42 |
43 | ## Gotchas
44 |
45 | These are some implementation gotchas which are welcome to be fixed if you can
46 | think of workarounds :)
47 |
48 | ### Routes should reflect data model
49 |
50 | For Hypermodel to generate `_links` correctly, the relationship/embedding
51 | structure of the model must match exactly that of the app routing. That means,
52 | that if you have `Blogs` that have many `Posts` which have many `Comments`,
53 | the routes.rb file should reflect it:
54 |
55 | ````ruby
56 | # config/routes.rb
57 | resources :blogs do
58 | resources :posts do
59 | resources :comments
60 | end
61 | end
62 | ````
63 |
64 | ### Every resource controller must implement the :show action at least
65 |
66 | Each resource and subresource must respond to the `show` action (so they can be
67 | linked from anywhere, in the `_links` section).
68 |
69 | ### The first belongs_to decides the hierarchy chain
70 |
71 | So if a `Post` belongs to an author and to a blog, and you want to access
72 | posts through blogs, not authors, you have to put the `belongs_to :blog`
73 | **before** `belongs_to :author`:
74 |
75 | ````ruby
76 | # app/models/post.rb
77 | class Post
78 | include Mongoid::Document
79 |
80 | belongs_to :blog
81 | belongs_to :author
82 | end
83 | ````
84 |
85 | I know, lame.
86 |
87 | ## Contributing
88 |
89 | * [List of hypermodel contributors][contributors]
90 |
91 | * Fork the project.
92 | * Make your feature addition or bug fix.
93 | * Add specs for it. This is important so we don't break it in a future
94 | version unintentionally.
95 | * Commit, do not mess with rakefile, version, or history.
96 | If you want to have your own version, that is fine but bump version
97 | in a commit by itself I can ignore when I pull.
98 | * Send me a pull request. Bonus points for topic branches.
99 |
100 | ## License
101 |
102 | MIT License. Copyright 2012 [Codegram Technologies][codegram]
103 |
104 | [mongoid]: http://mongoid.org
105 | [hal]: http://stateless.co/hal_specification.html
106 | [contributors]: https://github.com/codegram/hypermodel/contributors
107 | [codegram]: http://codegram.com
--------------------------------------------------------------------------------
/lib/hypermodel/serializers/mongoid.rb:
--------------------------------------------------------------------------------
1 | module Hypermodel
2 | module Serializers
3 | # Internal: A Mongoid serializer that complies with the Hypermodel
4 | # Serializer API.
5 | #
6 | # It is used by Hypermodel::Resource to extract the attributes and
7 | # resources of a given record.
8 | class Mongoid
9 |
10 | # Public: Returns the Mongoid instance
11 | attr_reader :record
12 |
13 | # Public: Returns the attributes of the Mongoid instance
14 | attr_reader :attributes
15 |
16 | # Public: Initializes a Serializer::Mongoid.
17 | #
18 | # record - A Mongoid instance of a model.
19 | def initialize(record)
20 | @record = record
21 | @attributes = record.attributes.dup
22 | end
23 |
24 | # Public: Returns a Hash with the resources that are linked to the
25 | # record. It will be used by Hypermodel::Resource.
26 | #
27 | # An example of a linked resource could be the author of a post. Think
28 | # of `/authors/:author_id`
29 | #
30 | # The format of the returned Hash must be the following:
31 | #
32 | # {resource_name: resource_instance}
33 | #
34 | # `resource_name` can be either a Symbol or a String.
35 | def resources
36 | relations = select_relations_by_type(::Mongoid::Relations::Referenced::In)
37 |
38 | relations.inject({}) do |acc, (name, _)|
39 | acc.update(name => @record.send(name))
40 | end
41 | end
42 |
43 | # Public: Returns a Hash with the sub resources that are linked to the
44 | # record. It will be used by Hypermodel::Resource. These resources need
45 | # to be differentiated so Hypermodel::Resource can build the url.
46 | #
47 | # An example of a linked sub resource could be comments of a post.
48 | # Think of `/posts/:id/comments`
49 | #
50 | # The format of the returned Hash must be the following:
51 | #
52 | # {:sub_resource, :another_subresource}
53 | def sub_resources
54 | select_relations_by_type(::Mongoid::Relations::Referenced::Many).keys
55 | end
56 |
57 | # Public: Returns a Hash with the embedded resources attributes. It will
58 | # be used by Hypermodel::Resource.
59 | #
60 | # An example of an embedded resource could be the reviews of a post, or
61 | # the addresses of a company. But you can really embed whatever you like.
62 | #
63 | # An example of the returning Hash could be the following:
64 | #
65 | # {"comments"=>
66 | # [
67 | # {"_id"=>"4fb941cb82b4d46162000007", "body"=>"Comment 1"},
68 | # {"_id"=>"4fb941cb82b4d46162000008", "body"=>"Comment 2"}
69 | # ]
70 | # }
71 | def embedded_resources
72 | return {} if embedded_relations.empty?
73 |
74 | embedded_relations.inject({}) do |acc, (name, metadata)|
75 | if attributes = extract_embedded_attributes(name)
76 | @attributes.delete(name)
77 | acc.update(name => attributes)
78 | end
79 | acc
80 | end
81 | end
82 |
83 | # Public: Returns a Hash with the resources that are embedding us (our
84 | # immediate ancestors).
85 | def embedding_resources
86 | return {} if embedded_relations.empty?
87 |
88 | embedded = select_embedded_by_type(::Mongoid::Relations::Embedded::In)
89 |
90 | embedded.inject({}) do |acc, (name, _)|
91 | acc.update(name => @record.send(name))
92 | end
93 | end
94 |
95 | #######
96 | private
97 | #######
98 |
99 | def select_relations_by_type(type)
100 | referenced_relations.select do |name, metadata|
101 | metadata.relation == type
102 | end
103 | end
104 |
105 | def select_embedded_by_type(type)
106 | embedded_relations.select do |name, metadata|
107 | metadata.relation == type
108 | end
109 | end
110 |
111 | def extract_embedded_attributes(name)
112 | embedded = @record.send(name)
113 |
114 | return {} unless embedded
115 | return embedded.map(&:attributes) if embedded.respond_to?(:map)
116 |
117 | embedded.attributes
118 | end
119 |
120 | def embedded_relations
121 | @embedded_relations ||= @record.relations.select do |_, metadata|
122 | metadata.relation.name =~ /Embedded/
123 | end
124 | end
125 |
126 | def referenced_relations
127 | @referenced_relations ||= @record.relations.select do |_, metadata|
128 | metadata.relation.name =~ /Referenced/
129 | end
130 | end
131 | end
132 | end
133 | end
134 |
--------------------------------------------------------------------------------
/test/dummy/README.rdoc:
--------------------------------------------------------------------------------
1 | == Welcome to Rails
2 |
3 | Rails is a web-application framework that includes everything needed to create
4 | database-backed web applications according to the Model-View-Control pattern.
5 |
6 | This pattern splits the view (also called the presentation) into "dumb"
7 | templates that are primarily responsible for inserting pre-built data in between
8 | HTML tags. The model contains the "smart" domain objects (such as Account,
9 | Product, Person, Post) that holds all the business logic and knows how to
10 | persist themselves to a database. The controller handles the incoming requests
11 | (such as Save New Account, Update Product, Show Post) by manipulating the model
12 | and directing data to the view.
13 |
14 | In Rails, the model is handled by what's called an object-relational mapping
15 | layer entitled Active Record. This layer allows you to present the data from
16 | database rows as objects and embellish these data objects with business logic
17 | methods. You can read more about Active Record in
18 | link:files/vendor/rails/activerecord/README.html.
19 |
20 | The controller and view are handled by the Action Pack, which handles both
21 | layers by its two parts: Action View and Action Controller. These two layers
22 | are bundled in a single package due to their heavy interdependence. This is
23 | unlike the relationship between the Active Record and Action Pack that is much
24 | more separate. Each of these packages can be used independently outside of
25 | Rails. You can read more about Action Pack in
26 | link:files/vendor/rails/actionpack/README.html.
27 |
28 |
29 | == Getting Started
30 |
31 | 1. At the command prompt, create a new Rails application:
32 | rails new myapp (where myapp is the application name)
33 |
34 | 2. Change directory to myapp and start the web server:
35 | cd myapp; rails server (run with --help for options)
36 |
37 | 3. Go to http://localhost:3000/ and you'll see:
38 | "Welcome aboard: You're riding Ruby on Rails!"
39 |
40 | 4. Follow the guidelines to start developing your application. You can find
41 | the following resources handy:
42 |
43 | * The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html
44 | * Ruby on Rails Tutorial Book: http://www.railstutorial.org/
45 |
46 |
47 | == Debugging Rails
48 |
49 | Sometimes your application goes wrong. Fortunately there are a lot of tools that
50 | will help you debug it and get it back on the rails.
51 |
52 | First area to check is the application log files. Have "tail -f" commands
53 | running on the server.log and development.log. Rails will automatically display
54 | debugging and runtime information to these files. Debugging info will also be
55 | shown in the browser on requests from 127.0.0.1.
56 |
57 | You can also log your own messages directly into the log file from your code
58 | using the Ruby logger class from inside your controllers. Example:
59 |
60 | class WeblogController < ActionController::Base
61 | def destroy
62 | @weblog = Weblog.find(params[:id])
63 | @weblog.destroy
64 | logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!")
65 | end
66 | end
67 |
68 | The result will be a message in your log file along the lines of:
69 |
70 | Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1!
71 |
72 | More information on how to use the logger is at http://www.ruby-doc.org/core/
73 |
74 | Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are
75 | several books available online as well:
76 |
77 | * Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe)
78 | * Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide)
79 |
80 | These two books will bring you up to speed on the Ruby language and also on
81 | programming in general.
82 |
83 |
84 | == Debugger
85 |
86 | Debugger support is available through the debugger command when you start your
87 | Mongrel or WEBrick server with --debugger. This means that you can break out of
88 | execution at any point in the code, investigate and change the model, and then,
89 | resume execution! You need to install ruby-debug to run the server in debugging
90 | mode. With gems, use sudo gem install ruby-debug. Example:
91 |
92 | class WeblogController < ActionController::Base
93 | def index
94 | @posts = Post.all
95 | debugger
96 | end
97 | end
98 |
99 | So the controller will accept the action, run the first line, then present you
100 | with a IRB prompt in the server window. Here you can do things like:
101 |
102 | >> @posts.inspect
103 | => "[#nil, "body"=>nil, "id"=>"1"}>,
105 | #"Rails", "body"=>"Only ten..", "id"=>"2"}>]"
107 | >> @posts.first.title = "hello from a debugger"
108 | => "hello from a debugger"
109 |
110 | ...and even better, you can examine how your runtime objects actually work:
111 |
112 | >> f = @posts.first
113 | => #nil, "body"=>nil, "id"=>"1"}>
114 | >> f.
115 | Display all 152 possibilities? (y or n)
116 |
117 | Finally, when you're ready to resume execution, you can enter "cont".
118 |
119 |
120 | == Console
121 |
122 | The console is a Ruby shell, which allows you to interact with your
123 | application's domain model. Here you'll have all parts of the application
124 | configured, just like it is when the application is running. You can inspect
125 | domain models, change values, and save to the database. Starting the script
126 | without arguments will launch it in the development environment.
127 |
128 | To start the console, run rails console from the application
129 | directory.
130 |
131 | Options:
132 |
133 | * Passing the -s, --sandbox argument will rollback any modifications
134 | made to the database.
135 | * Passing an environment name as an argument will load the corresponding
136 | environment. Example: rails console production.
137 |
138 | To reload your controllers and models after launching the console run
139 | reload!
140 |
141 | More information about irb can be found at:
142 | link:http://www.rubycentral.org/pickaxe/irb.html
143 |
144 |
145 | == dbconsole
146 |
147 | You can go to the command line of your database directly through rails
148 | dbconsole. You would be connected to the database with the credentials
149 | defined in database.yml. Starting the script without arguments will connect you
150 | to the development database. Passing an argument will connect you to a different
151 | database, like rails dbconsole production. Currently works for MySQL,
152 | PostgreSQL and SQLite 3.
153 |
154 | == Description of Contents
155 |
156 | The default directory structure of a generated Ruby on Rails application:
157 |
158 | |-- app
159 | | |-- assets
160 | | |-- images
161 | | |-- javascripts
162 | | `-- stylesheets
163 | | |-- controllers
164 | | |-- helpers
165 | | |-- mailers
166 | | |-- models
167 | | `-- views
168 | | `-- layouts
169 | |-- config
170 | | |-- environments
171 | | |-- initializers
172 | | `-- locales
173 | |-- db
174 | |-- doc
175 | |-- lib
176 | | `-- tasks
177 | |-- log
178 | |-- public
179 | |-- script
180 | |-- test
181 | | |-- fixtures
182 | | |-- functional
183 | | |-- integration
184 | | |-- performance
185 | | `-- unit
186 | |-- tmp
187 | | |-- cache
188 | | |-- pids
189 | | |-- sessions
190 | | `-- sockets
191 | `-- vendor
192 | |-- assets
193 | `-- stylesheets
194 | `-- plugins
195 |
196 | app
197 | Holds all the code that's specific to this particular application.
198 |
199 | app/assets
200 | Contains subdirectories for images, stylesheets, and JavaScript files.
201 |
202 | app/controllers
203 | Holds controllers that should be named like weblogs_controller.rb for
204 | automated URL mapping. All controllers should descend from
205 | ApplicationController which itself descends from ActionController::Base.
206 |
207 | app/models
208 | Holds models that should be named like post.rb. Models descend from
209 | ActiveRecord::Base by default.
210 |
211 | app/views
212 | Holds the template files for the view that should be named like
213 | weblogs/index.html.erb for the WeblogsController#index action. All views use
214 | eRuby syntax by default.
215 |
216 | app/views/layouts
217 | Holds the template files for layouts to be used with views. This models the
218 | common header/footer method of wrapping views. In your views, define a layout
219 | using the layout :default and create a file named default.html.erb.
220 | Inside default.html.erb, call <% yield %> to render the view using this
221 | layout.
222 |
223 | app/helpers
224 | Holds view helpers that should be named like weblogs_helper.rb. These are
225 | generated for you automatically when using generators for controllers.
226 | Helpers can be used to wrap functionality for your views into methods.
227 |
228 | config
229 | Configuration files for the Rails environment, the routing map, the database,
230 | and other dependencies.
231 |
232 | db
233 | Contains the database schema in schema.rb. db/migrate contains all the
234 | sequence of Migrations for your schema.
235 |
236 | doc
237 | This directory is where your application documentation will be stored when
238 | generated using rake doc:app
239 |
240 | lib
241 | Application specific libraries. Basically, any kind of custom code that
242 | doesn't belong under controllers, models, or helpers. This directory is in
243 | the load path.
244 |
245 | public
246 | The directory available for the web server. Also contains the dispatchers and the
247 | default HTML files. This should be set as the DOCUMENT_ROOT of your web
248 | server.
249 |
250 | script
251 | Helper scripts for automation and generation.
252 |
253 | test
254 | Unit and functional tests along with fixtures. When using the rails generate
255 | command, template test files will be generated for you and placed in this
256 | directory.
257 |
258 | vendor
259 | External libraries that the application depends on. Also includes the plugins
260 | subdirectory. If the app has frozen rails, those gems also go here, under
261 | vendor/rails/. This directory is in the load path.
262 |
--------------------------------------------------------------------------------