module RestfulJson::Controller
Constants
- NILS
- SINGLE_VALUE_ACTIONS
Public Class Methods
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.
# 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
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
# 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
# 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
# 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
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
# 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
# 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
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
# 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
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
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
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
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
# 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
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
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
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
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
# 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
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
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
# File lib/restful_json/controller.rb, line 409 def single_value_response? SINGLE_VALUE_ACTIONS.include?(params[:action].to_sym) end
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