lib/heimdallr/resource.rb in heimdallr-0.0.1 vs lib/heimdallr/resource.rb in heimdallr-0.0.2

- old
+ new

@@ -1,111 +1,251 @@ 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 - extend ActiveSupport::Concern + # @group Actions - module ClassMethods - attr_reader :model + # +GET /resources+ + # + # This action does nothing by itself, but it has a +load_all_resources+ filter attached. + def index + end - def resource_for(model, options={}) - @model = model.to_s.capitalize.constantize + # +GET /resource/1+ + # + # This action does nothing by itself, but it has a +load_one_resource+ filter attached. + def show + end - 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 + # +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 - module InstanceMethods - def index + # +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 - def show - end + render_resources + end - def new - render :json => { - :fields => model.restrictions(security_context).whitelist[:create] - } + # +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 <tt>params[:id]</tt>, + # 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 - def create - model.transaction do - if params.has_key? model.name.underscore - scoped_model.new.to_proxy(security_context, :create). - update_attributes!(params[model.name.underscore]) - else - @resources.each_with_index do |resource, index| - scoped_model.new.to_proxy(security_context, :create). - update_attributes!(params[model.name.underscore.pluralize][index]) - end - end - end + render_resources + end - if block_given? - yield - else - render_modified_resources - end + # +DELETE /resources/1,2+ + # + # This action destroys one or more records. It expects resource IDs to be passed + # comma-separated in <tt>params[:id]</tt>. + # + # See also {#load_referenced_resources}. + def destroy + model.transaction do + @resources.each &:destroy end - def edit - render :json => { - :fields => model.restrictions(security_context).whitelist[:update] - } - end + render :json => {}, :status => :ok + end - def update - model.transaction do - if params.has_key? model.name.underscore - @resources.first.to_proxy(security_context, :update). - update_attributes!(params[model.name.underscore]) - else - @resources.each_with_index do |resource, index| - resource.to_proxy(security_context, :update). - update_attributes!(params[model.name.underscore.pluralize][index]) - end - end - end + protected - if block_given? - yield - else - render_modified_resources - end - end + # @group Configuration - def destroy - model.transaction do - @resources.each &:destroy - end + # Return the associated model class. + # @return [Class] associated model + def model + self.class.model + end - render :json => {}, :status => :ok - 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 - protected + # Loads all resources in the current scope to +@resources+. + # + # Is automatically applied to {#index}. + def load_all_resources + @resources = scoped_model + end - def model - self.class.model - end + # Loads one resource from the current scope, referenced by <code>params[:id]</code>, + # to +@resource+. + # + # Is automatically applied to {#show}. + def load_one_resource + @resource = scoped_model.find(params[:id]) + end - def scoped_model - self.model.scoped - end + # Loads several resources from the current scope, referenced by <code>params[:id]</code> + # 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 - def load_one_resource - @resource = scoped_model.find(params[:id]) - 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 - def load_all_resources - @resources = scoped_model.scoped + # 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 - def load_referenced_resources - @resources = scoped_model.find(params[:id].split(',')) - end + extend ActiveSupport::Concern - def render_modified_resources - render :action => :index + # 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<Symbol>] :index + # Additional actions to be covered by {Heimdallr::Resource#load_all_resources}. + # @option options [Array<Symbol>] :member + # Additional actions to be covered by {Heimdallr::Resource#load_one_resource}. + # @option options [Array<Symbol>] :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 \ No newline at end of file