module RestfulJson::Controller

Constants

NILS
SINGLE_VALUE_ACTIONS

Public Class Methods

new() click to toggle source

In initialize we:

  • guess model name, if unspecified, from controller name

  • define instance variables containing model name

  • define the (model_plural_name)_url method, needed if controllers are not in the same module as the models

Note: if controller name is not based on model name and controller is in different module than model, you’ll need to redefine the appropriate method(s) to return urls if needed.

Calls superclass method
# File lib/restful_json/controller.rb, line 199
def initialize
  super

  # if not set, use controller classname
  qualified_controller_name = self.class.name.chomp('Controller')
  @model_class = self.model_class || qualified_controller_name.split('::').last.singularize.constantize

  raise "#{self.class.name} failed to initialize. self.model_class was nil in #{self} which shouldn't happen!" if @model_class.nil?
  raise "#{self.class.name} assumes that #{self.model_class} extends ActiveRecord::Base, but it didn't. Please fix, or remove this constraint." unless @model_class.ancestors.include?(ActiveRecord::Base)

  @model_singular_name = self.model_singular_name || self.model_class.name.underscore
  @model_plural_name = self.model_plural_name || @model_singular_name.pluralize
  @model_at_plural_name_sym = "@#{@model_plural_name}".to_sym
  @model_at_singular_name_sym = "@#{@model_singular_name}".to_sym
  
  # default methods for strong parameters
  @model_plural_name_params_sym = "#{@model_plural_name}_params".to_sym
  @model_singular_name_params_sym = "#{@model_singular_name}_params".to_sym

  @action_to_singular_action_model_params_method = {}
  @action_to_plural_action_model_params_method = {}

  underscored_modules_and_underscored_plural_model_name = qualified_controller_name.gsub('::','_').underscore

  # This is a workaround for controllers that are in a different module than the model only works if the controller's base part of the unqualified name in the plural model name.
  # If the model name is different than the controller name, you will need to define methods to return the right urls.
  class_eval "def #{@model_plural_name}_url;#{underscored_modules_and_underscored_plural_model_name}_url;end" unless @model_plural_name == underscored_modules_and_underscored_plural_model_name
  singularized_underscored_modules_and_underscored_plural_model_name = underscored_modules_and_underscored_plural_model_name
  class_eval "def #{@model_singular_name}_url(record);#{singularized_underscored_modules_and_underscored_plural_model_name}_url(record);end" unless @model_singular_name == singularized_underscored_modules_and_underscored_plural_model_name
end

Public Instance Methods

additional_render_or_respond_success_options() click to toggle source

Returns additional rendering options. By default will massage self.action_to_render_options a little and return that, e.g. if you had used serialize_action to specify an array and each serializer for a specific action, if it is that action, it may return something like: {serializer: MyFooArraySerializer, each_serializer: MyFooSerializer}. If you’d like to do something custom in some situations, but default in others, you may also call default_additional_render_or_respond_success_options from within this method to get the defaults.

# File lib/restful_json/controller.rb, line 418
def additional_render_or_respond_success_options
  default_additional_render_or_respond_success_options
end
allowed_params() click to toggle source
# File lib/restful_json/controller.rb, line 318
def allowed_params
  action_sym = params[:action].to_sym
  singular = single_value_response?
  action_specific_params_method = singular ? (@action_to_singular_action_model_params_method[action_sym] ||= "#{action_sym}_#{@model_singular_name}_params".to_sym) : (@action_to_plural_action_model_params_method[action_sym] ||= "#{action_sym}_#{@model_plural_name}_params".to_sym)
  model_name_params_method = singular ? @model_singular_name_params_sym : @model_plural_name_params_sym
  
  if self.actions_that_authorize.include?(action_sym)
    authorize! action_sym, @model_class
  end

  if self.actions_that_permit.include?(action_sym)
    if self.use_permitters
      return permitted_params_using(self.action_to_permitter[action_sym] || permitter_class)
    elsif self.allow_action_specific_params_methods && self.respond_to?(action_specific_params_method, true)
      return __send__(action_specific_params_method)
    elsif self.actions_supporting_params_methods.include?(action_sym)
      if self.respond_to?(model_name_params_method, true)
        return __send__(model_name_params_method)
      elsif defined?(::ActionController::StrongParameters)
        raise "#{self.class.name} needs a method (can be private): #{model_name_params_method}#{self.allow_action_specific_params_methods ? " or #{action_specific_params_method}" : ''}"
      end
    end
  end

  params
end
apply_includes(value) click to toggle source
# File lib/restful_json/controller.rb, line 310
def apply_includes(value)
  this_includes = current_action_includes
  if this_includes && this_includes.size > 0
    value = value.includes(*this_includes)
  end
  value
end
convert_request_param_value_for_filtering(attr_sym, value) click to toggle source
# File lib/restful_json/controller.rb, line 235
def convert_request_param_value_for_filtering(attr_sym, value)
  value && NILS.include?(value) ? nil : value
end
create() click to toggle source

The controller’s create (post) method to create a resource.

# File lib/restful_json/controller.rb, line 586
def create
  logger.debug "#{params[:action]} called in #{self.class}: model=#{@model_class}, request.format=#{request.format}, request.content_type=#{request.content_type}, params=#{params.inspect}" if self.debug
  @value = @model_class.new(allowed_params)
  @value.save
  instance_variable_set(@model_at_singular_name_sym, @value)
  render_or_respond(false, :created)
rescue self.rj_action_rescue_class => e
  handle_or_raise(e)
end
current_action_includes() click to toggle source
# File lib/restful_json/controller.rb, line 306
def current_action_includes
  self.action_to_query_includes[params[:action].to_sym] || self.query_includes
end
default_additional_render_or_respond_success_options() click to toggle source
# File lib/restful_json/controller.rb, line 422
def default_additional_render_or_respond_success_options
  result = {}
  if self.action_to_render_options[params[:action].to_sym]
    custom_action_serializer = self.action_to_render_options[params[:action].to_sym][:restful_json_serialization_default]
    result[(single_value_response? ? :serializer : :each_serializer)] = custom_action_serializer if custom_action_serializer
    custom_action_array_serializer = self.action_to_render_options[params[:action].to_sym][:restful_json_serialization_array]
    result[:serializer] = custom_action_array_serializer if custom_action_array_serializer && !single_value_response?
  end
  result
end
destroy() click to toggle source

The controller’s destroy (delete) method to destroy a resource.

# File lib/restful_json/controller.rb, line 611
def destroy
  logger.debug "#{params[:action]} called in #{self.class}: model=#{@model_class}, request.format=#{request.format}, request.content_type=#{request.content_type}, params=#{params.inspect}" if self.debug
  # don't raise error- DELETE should be idempotent per REST.
  @value = find_model_instance
  # allowed_params used primarily for authorization. can't do this to get id param(s) because those are sent via route, not
  # in wrapped params (if wrapped)
  allowed_params
  @value.destroy if @value
  instance_variable_set(@model_at_singular_name_sym, @value)
  if !@value.respond_to?(:errors) || @value.errors.empty? || (request.format != 'text/html' && request.content_type != 'text/html')
    # don't require a destroy view for success, because it isn't implements in Rails by default for json
    respond_to do |format|
      format.any  { head :ok }
    end
  else
    render_or_respond(false)
  end
rescue self.rj_action_rescue_class => e
  handle_or_raise(e)
end
do_find_model_instance(first_method) click to toggle source
# File lib/restful_json/controller.rb, line 276
def do_find_model_instance(first_method)
  # to_s as safety measure for vulnerabilities similar to CVE-2013-1854.
  # primary_key array support for composite_primary_keys.
  if @model_class.primary_key.is_a? Array
    c = @model_class
    c.primary_key.each {|pkey|c.where(pkey.to_sym => params[pkey].to_s)}
  else
    c = @model_class.where(@model_class.primary_key.to_sym => params[@model_class.primary_key].to_s)
  end

  c = apply_includes(c)
  @value = c.send first_method
end
edit() click to toggle source

The controller’s edit method (e.g. used for edit record in html format).

# File lib/restful_json/controller.rb, line 573
def edit
  logger.debug "#{params[:action]} called in #{self.class}: model=#{@model_class}, request.format=#{request.format}, request.content_type=#{request.content_type}, params=#{params.inspect}" if self.debug
  @value = find_model_instance!
  # allowed_params used primarily for authorization. can't do this to get id param(s) because those are sent via route, not
  # in wrapped params (if wrapped)
  allowed_params
  instance_variable_set(@model_at_singular_name_sym, @value)
  @value
rescue self.rj_action_rescue_class => e
  handle_or_raise(e)
end
exception_handling_data(e) click to toggle source

Searches through self.rj_action_rescue_handlers for appropriate handler. self.rj_action_rescue_handlers is an array of hashes where there is key :exception_classes and/or :exception_ancestor_classes along with :i18n_key and :status keys. :exception_classes contains an array of classes to exactly match the exception. :exception_ancestor_classes contains an array of classes that can match an ancestor of the exception. If exception handled, returns hash, hopefully containing keys :i18n_key and :status. Otherwise, returns nil which indicates that this exception should not be handled.

# File lib/restful_json/controller.rb, line 252
def exception_handling_data(e)
  self.rj_action_rescue_handlers.each do |handler|
    return handler if (handler.key?(:exception_classes) && handler[:exception_classes].include?(e.class))
    if handler.key?(:exception_ancestor_classes)
      handler[:exception_ancestor_classes].each do |ancestor|
        return handler if e.class.ancestors.include?(ancestor)
      end
    elsif !handler.key?(:exception_classes) && !handler.key?(:exception_ancestor_classes)
      return handler
    end
  end
  nil
end
find_model_instance() click to toggle source

Finds model using provided info in params, prior to any permittance, via where()…first.

Supports composite_keys.

# File lib/restful_json/controller.rb, line 294
def find_model_instance
  do_find_model_instance(:first)
end
find_model_instance!() click to toggle source

Finds model using provided info in params, prior to any permittance, via where()…first! with exception raise if does not exist.

Supports composite_keys.

# File lib/restful_json/controller.rb, line 302
def find_model_instance!
  do_find_model_instance(:first!)
end
handle_or_raise(e) click to toggle source
# File lib/restful_json/controller.rb, line 266
def handle_or_raise(e)
  raise e if self.rj_action_rescue_class.nil?
  handling_data = exception_handling_data(e)
  raise e unless handling_data
  # this is something we intended to rescue, so log it
  logger.error(e)
  # render error only if we haven't rendered response yet
  render_rj_action_error(e, handling_data) unless @performed_render
end
include_error_data?() click to toggle source

Returns self.return_error_data by default. To only return error_data in dev and test, use this: ‘def enable_long_error?; Rails.env.development? || Rails.env.test?; end`

# File lib/restful_json/controller.rb, line 241
def include_error_data?
  self.return_error_data
end
index() click to toggle source

The controller’s index (list) method to list resources.

Note: this method be alias_method’d by query_for, so it is more than just index.

# File lib/restful_json/controller.rb, line 436
def index
  # could be index or another action if alias_method'd by query_for
  logger.debug "#{params[:action]} called in #{self.class}: model=#{@model_class}, request.format=#{request.format}, request.content_type=#{request.content_type}, params=#{params.inspect}" if self.debug
  p_params = allowed_params
  t = @model_class.arel_table
  value = model_class_scoped
  custom_query = self.action_to_query[params[:action].to_sym]
  if custom_query
    value = custom_query.call(t, value)
  end

  value = apply_includes(value)

  self.param_to_query.each do |param_name, param_query|
    if params[param_name]
      # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
      value = param_query.call(t, value, p_params[param_name].to_s)
    end
  end

  self.param_to_through.each do |param_name, through_array|
    if p_params[param_name]
      # build query
      # e.g. SomeModel.all.joins({:assoc_name => {:sub_assoc => {:sub_sub_assoc => :sub_sub_sub_assoc}}).where(sub_sub_sub_assoc_model_table_name: {column_name: value})
      last_model_class = @model_class
      joins = nil # {:assoc_name => {:sub_assoc => {:sub_sub_assoc => :sub_sub_sub_assoc}}
      through_array.each do |association_or_attribute|
        if association_or_attribute == through_array.last
          # must convert param value to string before possibly using with ARel because of CVE-2013-1854, fixed in: 3.2.13 and 3.1.12
          # https://groups.google.com/forum/?fromgroups=#!msg/rubyonrails-security/jgJ4cjjS8FE/BGbHRxnDRTIJ
          value = value.joins(joins).where(last_model_class.table_name.to_sym => {association_or_attribute => p_params[param_name].to_s})
        else
          found_classes = last_model_class.reflections.collect {|association_name, reflection| reflection.class_name.constantize if association_name.to_sym == association_or_attribute}.compact
          if found_classes.size > 0
            last_model_class = found_classes[0]
          else
            # bad can_filter_by :through found at runtime
            raise "Association #{association_or_attribute.inspect} not found on #{last_model_class}."
          end

          if joins.nil?
            joins = association_or_attribute
          else
            joins = {association_or_attribute => joins}
          end
        end
      end
    end
  end

  self.param_to_attr_and_arel_predicate.keys.each do |param_name|
    options = param_to_attr_and_arel_predicate[param_name][2]
    # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
    param = p_params[param_name].to_s || options[:with_default]

    if param.present? && param_to_attr_and_arel_predicate[param_name]
      attr_sym = param_to_attr_and_arel_predicate[param_name][0]
      predicate_sym = param_to_attr_and_arel_predicate[param_name][1]
      if predicate_sym == :eq
        value = value.where(attr_sym => convert_request_param_value_for_filtering(attr_sym, param))
      else
        one_or_more_param = param.split(self.filter_split).collect{|v|convert_request_param_value_for_filtering(attr_sym, v)}
        value = value.where(t[attr_sym].try(predicate_sym, one_or_more_param))
      end
    end
  end

  if p_params[:page] && self.supported_functions.include?(:page)
    page = p_params[:page].to_i
    page = 1 if page < 1 # to avoid people using this as a way to get all records unpaged, as that probably isn't the intent?
    #TODO: to_s is hack to avoid it becoming an Arel::SelectManager for some reason which not sure what to do with
    value = value.skip((self.number_of_records_in_a_page * (page - 1)).to_s)
    value = value.take((self.number_of_records_in_a_page).to_s)
  end

  if p_params[:skip] && self.supported_functions.include?(:skip)
    # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
    value = value.skip(p_params[:skip].to_s)
  end

  if p_params[:take] && self.supported_functions.include?(:take)
    # to_s as safety measure for vulnerabilities similar to CVE-2013-1854
    value = value.take(p_params[:take].to_s)
  end

  if p_params[:uniq] && self.supported_functions.include?(:uniq)
    value = value.uniq
  end

  # these must happen at the end and are independent
  if p_params[:count] && self.supported_functions.include?(:count)
    value = value.count.to_i
  elsif p_params[:page_count] && self.supported_functions.include?(:page_count)
    count_value = value.count.to_i # this executes the query so nothing else can be done in AREL
    value = (count_value / self.number_of_records_in_a_page) + (count_value % self.number_of_records_in_a_page ? 1 : 0)
  else
    #TODO: also declaratively specify order via order=attr1,attr2, etc. like can_filter_by w/queries, subattrs, and direction.
    self.ordered_by.each do |attr_to_direction|
      # this looks nasty, but makes no sense to iterate keys if only single of each
      value = value.order(t[attr_to_direction.keys[0]].send(attr_to_direction.values[0]))
    end
    value = value.to_a
  end      
  @value = value
  instance_variable_set(@model_at_plural_name_sym, @value)
  render_or_respond(true)
rescue self.rj_action_rescue_class => e
  handle_or_raise(e)
end
model_class_scoped() click to toggle source

If Rails 3, returns @model_class.scoped. If not, returns @model_class.all. This helps avoid a deprecation warning.

# File lib/restful_json/controller.rb, line 231
def model_class_scoped
  Rails::VERSION::MAJOR == 3 ? @model_class.scoped : @model_class.all
end
new() click to toggle source

The controller’s new method (e.g. used for new record in html format).

# File lib/restful_json/controller.rb, line 560
def new
  logger.debug "#{params[:action]} called in #{self.class}: model=#{@model_class}, request.format=#{request.format}, request.content_type=#{request.content_type}, params=#{params.inspect}" if self.debug
  # allowed_params used primarily for authorization. can't do this to get id param(s) because those are sent via route, not
  # in wrapped params (if wrapped)
  allowed_params
  @value = @model_class.new
  instance_variable_set(@model_at_singular_name_sym, @value)
  render_or_respond(true)
rescue self.rj_action_rescue_class => e
  handle_or_raise(e)
end
render_or_respond(read_only_action, success_code = :ok) click to toggle source
# File lib/restful_json/controller.rb, line 377
def render_or_respond(read_only_action, success_code = :ok)
  if self.render_enabled
    # 404/not found is just for update (not destroy, because idempotent destroy = no 404)
    if success_code == :not_found
      respond_to do |format|
        format.html { render file: "#{Rails.root}/public/404.html", status: :not_found }
        format.any  { head :not_found }
      end
    elsif !@value.nil? && ((read_only_action && RestfulJson.return_resource) || RestfulJson.avoid_respond_with)
      respond_with(@value) do |format|
        format.json do
          if !@value.respond_to?(:errors) || @value.errors.empty?
            result = {json: @value, status: success_code}
            result.merge!(additional_render_or_respond_success_options)
          else
            result = {json: {errors: @value.errors}, status: :unprocessable_entity}
          end
          render result
        end
      end
    else
      if !@value.respond_to?(:errors) || @value.errors.empty?
        respond_with @value, additional_render_or_respond_success_options
      else
        respond_with @value
      end
    end
  else
    @value
  end
end
render_rj_action_error(e, handling_data) click to toggle source

Renders error using handling data options (where options are probably hash from self.rj_action_rescue_handlers that was matched).

If include_error_data? is true, it returns something like the following (with the appropriate HTTP status code via setting appropriate status in respond_do: {“status”: “not_found”,

"error": "Internationalized error message or e.message",
"error_data": {"type": "ActiveRecord::RecordNotFound", "message": "Couldn't find Bar with id=23423423", "trace": ["backtrace line 1", ...]}

}

If include_error_data? is false, it returns something like: {“status”: “not_found”, “error”, “Couldn’t find Bar with id=23423423”}

It handles any format in theory that is supported by respond_to and has a ‘to_(some format)` method.

# File lib/restful_json/controller.rb, line 357
def render_rj_action_error(e, handling_data)
  use_backtrace_cleaner = handling_data[:clean_backtrace] || true
  i18n_key = handling_data[:i18n_key]
  msg = t(i18n_key, default: e.message)
  status = handling_data[:status] || :internal_server_error
  if include_error_data?
    respond_to do |format|
      format.html { render notice: msg }
      format.any { render request.format.to_sym => {status: status, error: msg, error_data: {type: e.class.name, message: e.message, trace: Rails.backtrace_cleaner.clean(e.backtrace)}}, status: status }
    end
  else
    respond_to do |format|
      format.html { render notice: msg }
      format.any { render request.format.to_sym => {status: status, error: msg}, status: status }
    end
  end
  # return exception so we know it was handled
  e
end
show() click to toggle source

The controller’s show (get) method to return a resource.

# File lib/restful_json/controller.rb, line 547
def show
  logger.debug "#{params[:action]} called in #{self.class}: model=#{@model_class}, request.format=#{request.format}, request.content_type=#{request.content_type}, params=#{params.inspect}" if self.debug
  @value = find_model_instance!
  # allowed_params used primarily for authorization. can't do this to get id param(s) because those are sent via route, not
  # in wrapped params (if wrapped)
  allowed_params
  instance_variable_set(@model_at_singular_name_sym, @value)
  render_or_respond(true, @value.nil? ? :not_found : :ok)
rescue self.rj_action_rescue_class => e
  handle_or_raise(e)
end
single_value_response?() click to toggle source
# File lib/restful_json/controller.rb, line 409
def single_value_response?
  SINGLE_VALUE_ACTIONS.include?(params[:action].to_sym)
end
update() click to toggle source

The controller’s update (put) method to update a resource.

# File lib/restful_json/controller.rb, line 597
def update
  logger.debug "#{params[:action]} called in #{self.class}: model=#{@model_class}, request.format=#{request.format}, request.content_type=#{request.content_type}, params=#{params.inspect}" if self.debug
  @value = find_model_instance!
  # allowed_params used primarily for authorization. can't do this to get id param(s) because those are sent via route, not
  # in wrapped params (if wrapped)
  p_params = allowed_params
  @value.update_attributes(p_params) unless @value.nil?
  instance_variable_set(@model_at_singular_name_sym, @value)
  render_or_respond(true, @value.nil? ? :not_found : :ok)
rescue self.rj_action_rescue_class => e
  handle_or_raise(e)
end