Module: RestfulJson::Controller
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/restful_json/controller.rb
Defined Under Namespace
Modules: ClassMethods
Constant Summary collapse
- NILS =
['NULL'.freeze, 'null'.freeze, 'nil'.freeze]
- SINGLE_VALUE_ACTIONS =
[:create, :update, :destroy, :show, :new, :edit]
Instance Method Summary collapse
-
#additional_render_or_respond_success_options ⇒ Object
Returns additional rendering options.
- #allowed_params ⇒ Object
- #apply_includes(value) ⇒ Object
- #convert_request_param_value_for_filtering(attr_sym, value) ⇒ Object
-
#create ⇒ Object
The controller’s create (post) method to create a resource.
- #current_action_includes ⇒ Object
- #default_additional_render_or_respond_success_options ⇒ Object
-
#destroy ⇒ Object
The controller’s destroy (delete) method to destroy a resource.
- #do_find_model_instance(first_method) ⇒ Object
-
#edit ⇒ Object
The controller’s edit method (e.g. used for edit record in html format).
-
#exception_handling_data(e) ⇒ Object
Searches through self.rj_action_rescue_handlers for appropriate handler.
-
#find_model_instance ⇒ Object
Finds model using provided info in params, prior to any permittance, via where()…first.
-
#find_model_instance! ⇒ Object
Finds model using provided info in params, prior to any permittance, via where()…first! with exception raise if does not exist.
- #handle_or_raise(e) ⇒ Object
-
#include_error_data? ⇒ Boolean
Returns self.return_error_data by default.
-
#index ⇒ Object
The controller’s index (list) method to list resources.
-
#initialize ⇒ Object
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.
-
#model_class_scoped ⇒ Object
If Rails 3, returns @model_class.scoped.
-
#new ⇒ Object
The controller’s new method (e.g. used for new record in html format).
- #render_or_respond(read_only_action, success_code = :ok) ⇒ Object
-
#render_rj_action_error(e, handling_data) ⇒ Object
Renders error using handling data options (where options are probably hash from self.rj_action_rescue_handlers that was matched).
-
#show ⇒ Object
The controller’s show (get) method to return a resource.
- #single_value_response? ⇒ Boolean
-
#update ⇒ Object
The controller’s update (put) method to update a resource.
Instance Method Details
#additional_render_or_respond_success_options ⇒ Object
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: 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.
418 419 420 |
# File 'lib/restful_json/controller.rb', line 418 def end |
#allowed_params ⇒ Object
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 |
# 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..include?(action_sym) 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) ⇒ Object
310 311 312 313 314 315 316 |
# 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) ⇒ Object
235 236 237 |
# 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 ⇒ Object
The controller’s create (post) method to create a resource.
586 587 588 589 590 591 592 593 594 |
# 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 ⇒ Object
306 307 308 |
# 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 ⇒ Object
422 423 424 425 426 427 428 429 430 431 |
# File 'lib/restful_json/controller.rb', line 422 def result = {} if self.[params[:action].to_sym] custom_action_serializer = self.[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.[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 ⇒ Object
The controller’s destroy (delete) method to destroy a resource.
611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 |
# 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) ⇒ Object
276 277 278 279 280 281 282 283 284 285 286 287 288 |
# 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 ⇒ Object
The controller’s edit method (e.g. used for edit record in html format).
573 574 575 576 577 578 579 580 581 582 583 |
# 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) ⇒ Object
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.
252 253 254 255 256 257 258 259 260 261 262 263 264 |
# 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 ⇒ Object
Finds model using provided info in params, prior to any permittance, via where()…first.
Supports composite_keys.
294 295 296 |
# File 'lib/restful_json/controller.rb', line 294 def find_model_instance do_find_model_instance(:first) end |
#find_model_instance! ⇒ Object
Finds model using provided info in params, prior to any permittance, via where()…first! with exception raise if does not exist.
Supports composite_keys.
302 303 304 |
# File 'lib/restful_json/controller.rb', line 302 def find_model_instance! do_find_model_instance(:first!) end |
#handle_or_raise(e) ⇒ Object
266 267 268 269 270 271 272 273 274 |
# 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? ⇒ Boolean
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`
241 242 243 |
# File 'lib/restful_json/controller.rb', line 241 def include_error_data? self.return_error_data end |
#index ⇒ Object
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.
436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 |
# 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| = 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 || [: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 |
#initialize ⇒ Object
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.
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# 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 |
#model_class_scoped ⇒ Object
If Rails 3, returns @model_class.scoped. If not, returns @model_class.all. This helps avoid a deprecation warning.
231 232 233 |
# File 'lib/restful_json/controller.rb', line 231 def model_class_scoped Rails::VERSION::MAJOR == 3 ? @model_class.scoped : @model_class.all end |
#new ⇒ Object
The controller’s new method (e.g. used for new record in html format).
560 561 562 563 564 565 566 567 568 569 570 |
# 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) ⇒ Object
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 |
# 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!() 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, else respond_with @value end end else @value end end |
#render_rj_action_error(e, handling_data) ⇒ Object
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: “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.
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 |
# 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.) 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., 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 ⇒ Object
The controller’s show (get) method to return a resource.
547 548 549 550 551 552 553 554 555 556 557 |
# 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? ⇒ Boolean
409 410 411 |
# File 'lib/restful_json/controller.rb', line 409 def single_value_response? SINGLE_VALUE_ACTIONS.include?(params[:action].to_sym) end |
#update ⇒ Object
The controller’s update (put) method to update a resource.
597 598 599 600 601 602 603 604 605 606 607 608 |
# 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 |