module ModelApi::BaseController::InstanceMethods
Constants
- DEFAULT_PAGE_SIZE
- SIMPLE_ID_REGEX
- UUID_REGEX
Protected Instance Methods
admin?(opts = {})
click to toggle source
Indicates whether API should render administrator-only content in API responses
# File lib/model-api/base_controller.rb, line 297 def admin?(opts = {}) return opts[:admin] if opts.include?(:admin) param = request.params[:admin] param.present? && admin_user?(opts) && (param.to_i != 0 && params.to_s.strip.downcase != 'false') end
admin_content?(opts = {})
click to toggle source
Deprecated
# File lib/model-api/base_controller.rb, line 305 def admin_content?(opts = {}) admin?(opts) end
admin_user?(opts = {})
click to toggle source
Indicates whether user has access to data they do not own.
# File lib/model-api/base_controller.rb, line 284 def admin_user?(opts = {}) return opts[:admin_user] if opts.include?(:admin_user) user = current_user return nil if user.nil? [:admin_api_user?, :admin_user?, :admin?].each do |method| next unless user.respond_to?(method) opts[:admin_user] = user.send(method) rescue next break end opts[:admin_user] ||= false end
api_context()
click to toggle source
# File lib/model-api/base_controller.rb, line 44 def api_context @api_context ||= ModelApi::ApiContext.new(self) end
bad_payload(opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 362 def bad_payload(opts = {}) opts = opts.dup format = opts[:format] || identify_format opts[:message] ||= "A properly-formatted #{format.to_s.upcase} " \ 'payload was expected in the HTTP request body but not found' simple_error(:bad_request, opts.delete(:error) || 'Missing/invalid request body (payload)', opts) end
bad_request(error, message, opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 371 def bad_request(error, message, opts = {}) opts[:message] = message || 'This request is invalid for the resource in its present state' simple_error(:bad_request, error || 'Invalid API request', opts) end
base_admin_api_options()
click to toggle source
# File lib/model-api/base_controller.rb, line 240 def base_admin_api_options base_api_options.merge(admin: true, admin_only: true) end
base_api_options()
click to toggle source
# File lib/model-api/base_controller.rb, line 236 def base_api_options self.class.base_api_options end
collection_query(opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 221 def collection_query(opts = {}) opts = api_context.prepare_options(base_api_options.merge(opts)) klass = opts[:model_class] || model_class query = api_context.api_query(klass, opts) unless (opts.include?(:user_filter) && !opts[:user_filter]) || (admin? || filtered_by_foreign_key?(query)) || !opts[:user] query = api_context.user_query(query, opts[:user], opts.merge(model_class: klass)) end query end
common_headers()
click to toggle source
# File lib/model-api/base_controller.rb, line 396 def common_headers ModelApi::Utils.common_http_headers.each do |k, v| response.headers[k] = v end end
common_object_query(opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 214 def common_object_query(opts = {}) opts = api_context.prepare_options(opts) id_info = opts[:id_info] || id_info(opts) api_context.common_object_query(id_info[:id_attribute], id_info[:id_value], opts.merge(id_param: id_info[:id_param])) end
common_response_links(_opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 174 def common_response_links(_opts = {}) {} end
create_and_render_object(obj, opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 114 def create_and_render_object(obj, opts = {}) opts = api_context.prepare_options(opts) object_link_options = opts[:object_link_options] object_link_options[:action] = :show save_and_render_object(obj, get_operation(:create, opts), opts.merge(location_header: true)) end
current_user()
click to toggle source
# File lib/model-api/base_controller.rb, line 392 def current_user nil end
do_create(opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 94 def do_create(opts = {}) klass = opts[:model_class] || model_class return unless ensure_admin_if_admin_only(opts) unless klass.is_a?(Class) && klass < ActiveRecord::Base fail 'Unable to process object creation; Missing or invalid model class' end obj, opts = prepare_object_for_create(klass, opts) return bad_payload(class: klass) if opts[:bad_payload] create_and_render_object(obj, opts) end
do_destroy(obj, opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 153 def do_destroy(obj, opts = {}) return unless ensure_admin_if_admin_only(opts) opts = api_context.prepare_options(opts) obj = obj.first if obj.is_a?(ActiveRecord::Relation) add_hateoas_links_for_update(opts) unless obj.present? return not_found(opts.merge(class: klass, field: :id)) end operation = opts[:operation] = get_operation(:destroy, opts) ModelApi::Utils.validate_operation(obj, operation, opts.merge(model_metadata: opts[:api_model_metadata] || opts[:model_metadata])) response_status, errs_or_msgs = Utils.process_object_destroy(obj, operation, opts) add_hateoas_links_for_updated_object(operation, opts) klass = ModelApi::Utils.find_class(obj, opts) ModelApi::Renderer.render(self, obj, opts.merge(status: response_status, root: ModelApi::Utils.model_name(klass).singular, messages: errs_or_msgs)) end
do_update(obj, opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 121 def do_update(obj, opts = {}) return unless ensure_admin_if_admin_only(opts) obj, opts = prepare_object_for_update(obj, opts) return bad_payload(class: klass) if opts[:bad_payload] unless obj.present? return not_found(opts.merge(class: ModelApi::Utils.find_class(obj, opts), field: :id)) end update_and_render_object(obj, opts) end
ensure_admin()
click to toggle source
# File lib/model-api/base_controller.rb, line 244 def ensure_admin return true if admin_user? # Mask presence of endpoint if user is not authorized to access it not_found false end
ensure_admin_if_admin_only(opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 407 def ensure_admin_if_admin_only(opts = {}) return true unless opts[:admin_only] ensure_admin end
filter_by_user()
click to toggle source
# File lib/model-api/base_controller.rb, line 388 def filter_by_user current_user end
get_operation(default_operation, opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 412 def get_operation(default_operation, opts = {}) if opts.key?(:operation) return opts[:operation] elsif action_name.start_with?('create') return :create elsif action_name.start_with?('update') return :update elsif action_name.start_with?('patch') return :patch elsif action_name.start_with?('destroy') return :destroy else return default_operation end end
handle_api_exceptions(err)
click to toggle source
# File lib/model-api/base_controller.rb, line 267 def handle_api_exceptions(err) if err.is_a?(ModelApi::NotFoundException) not_found(field: err.field, message: err.message) elsif err.is_a?(ModelApi::UnauthorizedException) unauthorized else return false end true end
id_info(opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 206 def id_info(opts = {}) id_info = {} id_info[:id_attribute] = (opts[:id_attribute] || :id).to_sym id_info[:id_param] = (opts[:id_param] || :id).to_sym id_info[:id_value] = (opts[:id_value] || params[id_info[:id_param]]).to_s id_info end
identify_format()
click to toggle source
# File lib/model-api/base_controller.rb, line 402 def identify_format format = self.request.format.symbol rescue :json format == :xml ? :xml : :json end
initialize_options(opts)
click to toggle source
# File lib/model-api/base_controller.rb, line 178 def initialize_options(opts) return opts if opts[:options_initialized] opts = opts.symbolize_keys opts[:api_context] ||= @api_context opts[:model_class] ||= model_class opts[:user] ||= filter_by_user opts[:user_id] ||= opts[:user].try(:id) opts[:admin_user] ||= admin_user?(opts) opts[:admin] ||= admin?(opts) unless opts.include?(:collection_link_options) && opts.include?(:object_link_options) default_link_options = request.params.to_h.symbolize_keys opts[:collection_link_options] ||= default_link_options opts[:object_link_options] ||= default_link_options if default_link_options[:exclude_associations].present? opts[:exclude_associations] ||= default_link_options[:exclude_associations] end end opts[:options_initialized] ||= true opts end
model_class()
click to toggle source
# File lib/model-api/base_controller.rb, line 40 def model_class self.class.model_class end
not_found(opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 355 def not_found(opts = {}) opts = opts.dup opts[:message] ||= 'No resource found at the path provided or matching the criteria ' \ 'specified' simple_error(:not_found, opts.delete(:error) || 'No resource found', opts) end
not_implemented(opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 382 def not_implemented(opts = {}) opts = opts.dup opts[:message] ||= 'This API feature is presently unavailable' simple_error(:not_implemented, opts.delete(:error) || 'Not implemented', opts) end
object_query(opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 232 def object_query(opts = {}) common_object_query(api_context.prepare_options(base_api_options.merge(opts))) end
parse_request_body()
click to toggle source
# File lib/model-api/base_controller.rb, line 428 def parse_request_body unless instance_variable_defined?(:@request_body) @req_body, @format = ModelApi::Utils.parse_request_body(request) end [@req_body, @format] end
prepare_object_for_create(klass, opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 105 def prepare_object_for_create(klass, opts = {}) opts = api_context.prepare_options(opts) req_body, format = parse_request_body opts = add_hateoas_links_for_update(opts) api_context.get_updated_object(klass, get_operation(:create, opts), req_body, opts.merge(format: format)) end
prepare_object_for_update(obj, opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 131 def prepare_object_for_update(obj, opts = {}) opts = api_context.prepare_options(opts) req_body, format = parse_request_body opts = add_hateoas_links_for_update(opts) api_context.get_updated_object(obj, get_operation(:update, opts), req_body, opts.merge(format: format)) end
prepare_options(opts)
click to toggle source
Default implementation, can be hidden by API controller classes to include any application-specific options
# File lib/model-api/base_controller.rb, line 201 def prepare_options(opts) return opts if opts[:options_initialized] initialize_options(opts) end
render_collection(collection, opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 48 def render_collection(collection, opts = {}) return unless ensure_admin_if_admin_only(opts) opts = api_context.prepare_options(opts) opts[:operation] ||= :index return unless api_context.validate_read_operation(collection, opts[:operation], opts) coll_route = opts[:collection_route] || self collection_links = { self: coll_route } collection = ModelApi::Utils.process_collection_includes(collection, opts.merge(model_metadata: opts[:api_model_metadata] || opts[:model_metadata])) collection, _result_filters = api_context.filter_collection(collection, find_filter_params, opts) collection, _result_sorts = api_context.sort_collection(collection, find_sort_params, opts) collection, collection_links, opts = paginate_collection(collection, collection_links, opts, coll_route) opts[:collection_links] = collection_links.merge(opts[:collection_links] || {}) .reverse_merge(common_response_links(opts)) add_collection_object_route(opts) ModelApi::Renderer.render(self, collection, opts) end
render_object(obj, opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 70 def render_object(obj, opts = {}) return unless ensure_admin_if_admin_only(opts) opts = api_context.prepare_options(opts) klass = ModelApi::Utils.find_class(obj, opts) object_route = opts[:object_route] || self opts[:object_links] = { self: object_route } if obj.is_a?(ActiveRecord::Base) return unless api_context.validate_read_operation(obj, opts[:operation], opts) unless obj.present? return not_found(opts.merge(class: klass, field: :id)) end opts[:object_links].merge!(opts[:object_links] || {}) else return not_found(opts) if obj.nil? obj = ModelApi::Utils.ext_value(obj, opts) unless opts[:raw_output] opts[:object_links].merge!(opts[:links] || {}) end opts[:operation] ||= :show opts[:object_links].reverse_merge!(common_response_links(opts)) ModelApi::Renderer.render(self, obj, opts) end
resource_parent_id(parent_model_class, opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 309 def resource_parent_id(parent_model_class, opts = {}) opts = api_context.prepare_options(opts) id_info = id_info(opts.reverse_merge(id_param: "#{parent_model_class.name.underscore}_id")) model_name = parent_model_class.model_name.human if id_info[:id_value].blank? unless opts[:optional] fail ModelApi::NotFoundException.new(id_info[:id_param], "#{model_name} not found") end return nil end query = common_object_query(opts.merge(model_class: parent_model_class, id_info: id_info)) parent_id = query.pluck(:id).first if parent_id.blank? unless opts[:optional] fail ModelApi::NotFoundException.new(id_info[:id_param], "#{model_name} '#{id_info[:id_value]}' not found") end return nil end parent_id end
save_and_render_object(obj, operation, opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 144 def save_and_render_object(obj, operation, opts = {}) opts = api_context.prepare_options(opts) status, msgs = Utils.process_updated_model_save(obj, operation, opts) add_hateoas_links_for_updated_object(operation, opts) successful = ModelApi::Utils.response_successful?(status) ModelApi::Renderer.render(self, successful ? obj : opts[:request_obj], opts.merge(status: status, operation: :show, messages: msgs)) end
simple_error(status, error, opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 331 def simple_error(status, error, opts = {}) opts = opts.dup klass = opts[:class] opts[:root] = ModelApi::Utils.model_name(klass).singular if klass.present? if error.is_a?(Array) errs_or_msgs = error.map do |e| if e.is_a?(Hash) next e if e.include?(:error) && e.include?(:message) next e.reverse_merge( error: e[:error] || 'Unspecified error', message: e[:message] || e[:error] || 'Unspecified error') end { error: e.to_s, message: e.to_s } end elsif error.is_a?(Hash) errs_or_msgs = [error] else errs_or_msgs = [{ error: error, message: opts[:message] || error }] end errs_or_msgs[0][:field] = opts[:field] if opts.include?(:field) ModelApi::Renderer.render(self, opts[:request_obj], opts.merge(status: status, messages: errs_or_msgs)) end
unhandled_exception(err)
click to toggle source
# File lib/model-api/base_controller.rb, line 252 def unhandled_exception(err) return if handle_api_exceptions(err) return if performed? error_details = {} if Rails.env == 'development' error_details[:message] = "Exception: #{err.message}" error_details[:backtrace] = err.backtrace else error_details[:message] = 'An internal server error has occurred ' \ 'while processing your request.' end ModelApi::Renderer.render(self, error_details, root: :error_details, status: :internal_server_error) end
update_and_render_object(obj, opts = {})
click to toggle source
# File lib/model-api/base_controller.rb, line 139 def update_and_render_object(obj, opts = {}) opts = api_context.prepare_options(opts) save_and_render_object(obj, get_operation(:update, opts), opts) end
Private Instance Methods
add_collection_object_route(opts)
click to toggle source
# File lib/model-api/base_controller.rb, line 542 def add_collection_object_route(opts) object_route = opts[:object_route] unless object_route.present? route_name = ModelApi::Utils.route_name(request) if route_name.present? if (singular_route_name = route_name.singularize) != route_name object_route = singular_route_name end end end if object_route.present? && (object_route.is_a?(String) || object_route.is_a?(Symbol)) object_route = nil unless self.respond_to?("#{object_route}_url") end object_route = opts[:default_object_route] if object_route.blank? return if object_route.blank? opts[:object_links] = (opts[:object_links] || {}).merge(self: object_route) end
add_hateoas_links_for_update(opts)
click to toggle source
# File lib/model-api/base_controller.rb, line 560 def add_hateoas_links_for_update(opts) object_route = opts[:object_route] || self links = { self: object_route }.reverse_merge(common_response_links(opts)) opts[:links] = links.merge(opts[:links] || {}) opts end
add_hateoas_links_for_updated_object(_operation, opts)
click to toggle source
# File lib/model-api/base_controller.rb, line 567 def add_hateoas_links_for_updated_object(_operation, opts) object_route = opts[:object_route] || self object_links = { self: object_route } opts[:object_links] = object_links.merge(opts[:object_links] || {}) end
add_pagination_links(collection_links, coll_route, page, last_page)
click to toggle source
# File lib/model-api/base_controller.rb, line 533 def add_pagination_links(collection_links, coll_route, page, last_page) if page < last_page collection_links[:next] = [coll_route, { page: (page + 1) }] end collection_links[:prev] = [coll_route, { page: (page - 1) }] if page > 1 collection_links[:first] = [coll_route, { page: 1 }] collection_links[:last] = [coll_route, { page: last_page }] end
filtered_by_foreign_key?(query)
click to toggle source
# File lib/model-api/base_controller.rb, line 573 def filtered_by_foreign_key?(query) fk_cache = self.class.instance_variable_get(:@foreign_key_cache) self.class.instance_variable_set(:@foreign_key_cache, fk_cache = {}) if fk_cache.nil? klass = query.klass foreign_keys = (fk_cache[klass] ||= query.klass.reflections.values .select { |a| a.macro == :belongs_to }.map { |a| a.foreign_key.to_s }) (query.values[:where] || []).select { |v| v.is_a?(Arel::Nodes::Equality) } .map { |v| v.left.name }.each do |key| return true if foreign_keys.include?(key) end false rescue Exception => e Rails.logger.warn "Exception encounterd determining if query is filtered: #{e.message}\n" \ "#{e.backtrace.join("\n")}" end
find_filter_params()
click to toggle source
# File lib/model-api/base_controller.rb, line 437 def find_filter_params request.params.reject { |p, _v| %w(access_token sort_by admin).include?(p) } end
find_sort_params()
click to toggle source
# File lib/model-api/base_controller.rb, line 441 def find_sort_params sort_by = params[:sort_by] return {} if sort_by.blank? sort_by = sort_by.to_s.strip if sort_by.starts_with?('{') || sort_by.starts_with?('[') process_json_sort_params(sort_by) else process_simple_sort_params(sort_by) end end
paginate_collection(collection, collection_links, opts, coll_route)
click to toggle source
# File lib/model-api/base_controller.rb, line 503 def paginate_collection(collection, collection_links, opts, coll_route) collection_size = collection.count page_size = (params[:page_size] || DEFAULT_PAGE_SIZE).to_i page = [params[:page].to_i, 1].max page_count = [(collection_size + page_size - 1) / page_size, 1].max page = page_count if page > page_count offset = (page - 1) * page_size opts = opts.dup opts[:count] ||= collection_size opts[:page] ||= page opts[:page_size] ||= page_size opts[:page_count] ||= page_count response.headers['X-Total-Count'] = collection_size.to_s opts[:collection_link_options] = (opts[:collection_link_options] || {}) .reject { |k, _v| [:page].include?(k.to_sym) } opts[:object_link_options] = (opts[:object_link_options] || {}) .reject { |k, _v| [:page, :page_size].include?(k.to_sym) } if collection_size > page_size opts[:collection_link_options][:page] = page add_pagination_links(collection_links, coll_route, page, page_count) collection = collection.limit(page_size).offset(offset) end [collection, collection_links, opts] end
process_json_sort_params(sort_by)
click to toggle source
# File lib/model-api/base_controller.rb, line 452 def process_json_sort_params(sort_by) sort_params = {} sort_json_obj = (JSON.parse(sort_by) rescue {}) sort_json_obj = Hash[sort_json_obj.map { |v| [v, nil] }] if sort_json_obj.is_a?(Array) sort_json_obj.each do |key, value| next if key.blank? value_lc = value.to_s.downcase if %w(a asc ascending).include?(value_lc) order = :asc elsif %w(d desc descending).include?(value_lc) order = :desc else order = :default end sort_params[key] = order end sort_params end
process_simple_sort_params(sort_by)
click to toggle source
# File lib/model-api/base_controller.rb, line 471 def process_simple_sort_params(sort_by) sort_params = {} sort_by.split(',').each do |key| key = key.to_s.strip key_lc = key.downcase if key_lc.ends_with?('_a') || key_lc.ends_with?(' a') key = sort_by[key[0..-3]] order = :asc elsif key_lc.ends_with?('_asc') || key_lc.ends_with?(' asc') key = sort_by[key[0..-5]] order = :asc elsif key_lc.ends_with?('_ascending') || key_lc.ends_with?(' ascending') key = sort_by[key[0..-11]] order = :asc elsif key_lc.ends_with?('_d') || key_lc.ends_with?(' d') key = sort_by[key[0..-3]] order = :desc elsif key_lc.ends_with?('_desc') || key_lc.ends_with?(' desc') key = sort_by[key[0..-6]] order = :desc elsif key_lc.ends_with?('_descending') || key_lc.ends_with?(' descending') key = sort_by[key[0..-12]] order = :desc else order = :default end next if key.blank? sort_params[key] = order end sort_params end