module Heimdallr
#
# @deprecated Will be removed ASAP. Please don't use it in favour of http://github.com/roundlake/heimdallr-resource/
#
# Heimdallr {LegacyResource} 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 LegacyResource
# @group Actions
# +GET /resources+
#
# This action does nothing by itself, but it has a +load_all_resources+ filter attached.
def index
render_data
end
# +GET /resource/1+
#
# This action does nothing by itself, but it has a +load_one_resource+ filter attached.
def show
render_data
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_data}.
#
# See also {#load_referenced_resources} and {#with_objects_from_params}.
def create
with_objects_from_params(replace: true) do |object, attributes|
restricted_model.create(attributes)
end
render_data verify: true
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.restrictions(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_data}.
#
# See also {#load_referenced_resources} and {#with_objects_from_params}.
def update
with_objects_from_params do |object, attributes|
object.update_attributes attributes
end
render_data verify: true
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
with_objects_from_params do |object, attributes|
object.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
# Return the scoped and restricted model. By default this method
# restricts the result of {#scoped_model} with +security_context+,
# which is expected to be defined on this class or its ancestors.
def restricted_model
scoped_model.restrict(security_context, implicit: true)
end
# Loads all resources in the current scope to +@resources+.
#
# Is automatically applied to {#index}.
def load_all_resources
@multiple_resources = true
@resources = restricted_model
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 {#show}, {#update} and {#destroy}.
def load_referenced_resources
if params[:id][0] == '*'
@multiple_resources = true
@resources = restricted_model.find(params[:id][1..-1].split(','))
else
@multiple_resources = false
@resource = restricted_model.find(params[:id])
end
end
# Render a modified collection in {#create}, {#update} and similar actions.
def render_data(options={})
if @multiple_resources
if options[:verify] && @resources.any?(&:invalid?)
render :json => { errors: @resources.map(&:errors) }, :status => :unprocessable_entity
else
render :action => :index
end
else
if options[:verify] && @resource.invalid?
render :json => @resource.errors, :status => :unprocessable_entity
else
render :action => :show
end
end
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(options={})
model.transaction do
if @multiple_resources
begin
name = model.name.underscore.pluralize
if params[name].is_a? Hash
enumerator = params[name].keys.each
else
enumerator = params[name].each_index
end
result = enumerator.map do |index|
yield(@resources[index.to_i], params[name][index])
end
ensure
@resources = result if options[:replace]
end
else
begin
result = yield(@resource, params[model.name.underscore])
ensure
@resource = result if options[:replace]
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 +ActiveRecord+-derived 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_referenced_resources, only: [ :show, :update, :destroy ].concat(options[:referenced] || [])
end
end
end
end