module Heimdallr # Heimdallr {Resource} is a boilerplate for simple creation of REST endpoints, most of which are # quite similar and thus may share a lot of code. # # The minimal controller possible would be: # # class MiceController < ApplicationController # include Heimdallr::Resource # # # Class Mouse must include Heimdallr::Model. # resource_for :mouse # end # # Resource is built with Convention over Configuration principle in mind; that is, # instead of providing complex configuration syntax, Resource consists of a lot of small, easy # to override methods. If some kind of default behavior is undesirable, then one can just override # the relative method in the particular controller or, say, define a module if the changes are # to be shared between several controllers. You are encouraged to explore the source of this class. # # Resource allows to perform efficient operations on collections of objects. The # {#create}, {#update} and {#destroy} actions accept both a single object/ID or an array of # objects/IDs. The cardinal _modus # # Resource expects a method named +security_context+ to be defined either in the controller itself # or, more conveniently, in any of its ancestors, likely +ApplicationController+. This method can # often be aliased to +current_user+. # # Resource only works with ActiveRecord. # # See also {Resource::ClassMethods}. module Resource # @group Actions # +GET /resources+ # # This action does nothing by itself, but it has a +load_all_resources+ filter attached. def index end # +GET /resource/1+ # # This action does nothing by itself, but it has a +load_one_resource+ filter attached. def show end # +GET /resources/new+ # # This action renders a JSON representation of fields whitelisted for creation. # It does not include any fixtures or validations. # # @example # { 'fields': [ 'topic', 'content' ] } def new render :json => { :fields => model.restrictions(security_context).allowed_fields[:create] } end # +POST /resources+ # # This action creates one or more records from the passed parameters. # It can accept both arrays of attribute hashes and single attribute hashes. # # After the creation, it calls {#render_resources}. # # See also {#load_referenced_resources} and {#with_objects_from_params}. def create with_objects_from_params do |attributes, index| scoped_model.restrict(security_context). create!(attributes) end render_resources end # +GET /resources/1/edit+ # # This action renders a JSON representation of fields whitelisted for updating. # See also {#new}. def edit render :json => { :fields => model.restrict(security_context).allowed_fields[:update] } end # +PUT /resources/1,2+ # # This action updates one or more records from the passed parameters. # It expects resource IDs to be passed comma-separated in params[:id], # and expects them to be in the order corresponding to the order of actual # attribute hashes. # # After the updating, it calls {#render_resources}. # # See also {#load_referenced_resources} and {#with_objects_from_params}. def update with_objects_from_params do |attributes, index| @resources[index].update_attributes! attributes end render_resources end # +DELETE /resources/1,2+ # # This action destroys one or more records. It expects resource IDs to be passed # comma-separated in params[:id]. # # See also {#load_referenced_resources}. def destroy model.transaction do @resources.each &:destroy end render :json => {}, :status => :ok end protected # @group Configuration # Return the associated model class. # @return [Class] associated model def model self.class.model end # Return the appropriately scoped model. By default this method # delegates to +self.model.scoped+; you may override it for nested # resources so that it would only return the nested set. # # For example, this code would not allow user to perform any actions # with a transaction from a wrong account, raising RecordNotFound # instead: # # # transactions_controller.rb # class TransactionsController < ApplicationController # include Heimdallr::Resource # # resource_for :transactions # # protected # # def scoped_model # Account.find(params[:account_id]).transactions # end # end # # # routes.rb # Foo::Application.routes.draw do # resources :accounts do # resources :transactions # end # end # # @return ActiveRecord scope def scoped_model self.model.scoped end # Loads all resources in the current scope to +@resources+. # # Is automatically applied to {#index}. def load_all_resources @resources = scoped_model end # Loads one resource from the current scope, referenced by params[:id], # to +@resource+. # # Is automatically applied to {#show}. def load_one_resource @resource = scoped_model.find(params[:id]) end # Loads several resources from the current scope, referenced by params[:id] # with a comma-separated string like "1,2,3", to +@resources+. # # Is automatically applied to {#update} and {#destroy}. def load_referenced_resources @resources = scoped_model.find(params[:id].split(',')) end # Render a modified collection in {#create}, {#update} and similar actions. # # By default, it invokes a template for +index+. def render_resources render :action => :index end # Fetch one or several objects passed in +params+ and yield them to a block, # wrapping everything in a transaction. # # @yield [attributes, index] # @yieldparam [Hash] attributes # @yieldparam [Integer] index def with_objects_from_params model.transaction do if params.has_key? model.name.underscore yield params[model.name.underscore], 0 else params[model.name.underscore.pluralize]. each_with_index do |attributes, index| yield attributes, index end end end end extend ActiveSupport::Concern # Class methods for {Heimdallr::Resource}. See also +ActiveSupport::Concern+. module ClassMethods # Returns the attached model class. # @return [Class] attr_reader :model # Attaches this resource to a model. # # Note that ActiveSupport +before_filter+ _replaces_ the list of actions for specified # filter and not appends to it. For example, the following code will only run +filter_a+ # when +bar+ action is invoked: # # class FooController < ApplicationController # before_filter :filter_a, :only => :foo # before_filter :filter_a, :only => :bar # # def foo; end # def bar; end # end # # For convenience, you can pass additional actions to register with default filters in # +options+. It is also possible to use +append_before_filter+. # # @param [Class] model An ActiveModel or ActiveRecord model class. # @option options [Array] :index # Additional actions to be covered by {Heimdallr::Resource#load_all_resources}. # @option options [Array] :member # Additional actions to be covered by {Heimdallr::Resource#load_one_resource}. # @option options [Array] :collection # Additional actions to be covered by {Heimdallr::Resource#load_referenced_resources}. def resource_for(model, options={}) @model = model.to_s.camelize.constantize before_filter :load_all_resources, only: [ :index ].concat(options[:all] || []) before_filter :load_one_resource, only: [ :show ].concat(options[:member] || []) before_filter :load_referenced_resources, only: [ :update, :destroy ].concat(options[:collection] || []) end end end end