-
-
Save ErvalhouS/31f62edb9ea704d3abf8269922ded732 to your computer and use it in GitHub Desktop.
# frozen_string_literal: true | |
module Api | |
module V1 | |
# Controller to consume read-only data to be used on client's frontend | |
class FrontEndController < ActionController::API | |
prepend_before_action :set_root_resource | |
before_action :set_object, except: %i[index schema] | |
append_before_action :set_nested_resource, only: %i[nested_index] | |
append_before_action :set_records, only: %i[index nested_index] | |
append_before_action :set_schema, only: %i[schema] | |
include Orderable | |
# GET /:resource | |
# GET /:resource.json | |
def index | |
render json: records_json | |
end | |
# GET /:resource/1 | |
# GET /:resource/1.json | |
def show | |
render json: @object.to_json(include: parsed_include) | |
end | |
# GET /:resource/1/:nested_resource | |
# GET /:resource/1/:nested_resource.json | |
def nested_index | |
render json: records_json | |
end | |
# OPTIONS /:resource | |
# OPTIONS /:resource.json | |
# OPTIONS /:resource/1/:nested_resource | |
# OPTIONS /:resource/1/:nested_resource.json | |
def schema | |
render json: @schemated.to_json | |
end | |
private | |
# Common setup to stablish which model is the resource of this request | |
def set_root_resource | |
@root_resource = params[:resource].classify.constantize | |
end | |
# Common setup to stablish which object this request is querying | |
def set_object | |
id = params[:id] | |
@object = resource.friendly.find(id) | |
rescue NoMethodError | |
@object = resource.find(id) | |
end | |
# Setup to stablish the nested model to be queried | |
def set_nested_resource | |
@nested_resource = @object.send(params[:nested]) | |
end | |
# Used to setup the resource's schema | |
def set_schema | |
@schemated = {} | |
raise ActionController::BadRequest('Invalid resource') unless resource.present? | |
resource.columns_hash.each { |key, value| @schemated[key] = value.type } | |
end | |
# Used to setup the records from the selected resource | |
# that are going to be rendered | |
def set_records | |
@records = resource.order(ordering_params(params)) | |
.ransack(parsed_query).result | |
end | |
# Used to avoid errors in JSON parsing | |
def parsed_query | |
JSON.parse(params[:q]) | |
rescue JSON::ParserError, TypeError | |
{} | |
end | |
# Used to avoid errors in included associations parsing | |
def parsed_include | |
params[:include].split(',') | |
rescue NoMethodError | |
[] | |
end | |
# Parsing of `@records` variable to paginated JSON | |
def records_json | |
@records.paginate(page: params[:page], per_page: params[:per_page]) | |
.to_json(include: parsed_include) | |
end | |
# Reutrns root_resource if nested_resource is not set | |
def resource | |
@nested_resource || @root_resource | |
end | |
end | |
end | |
end |
# frozen_string_literal: true | |
# This concern is used to provide abstract ordering based on `params[:sort]` | |
module Orderable | |
extend ActiveSupport::Concern | |
SORT_ORDER = { '+' => :asc, '-' => :desc }.freeze | |
# A list of the param names that can be used for ordering the model list | |
def ordering_params(params) | |
# For example it retrieves a list of orders in descending order of total_value. | |
# Within a specific total_value, older orders are ordered first | |
# | |
# GET /orders?sort=-total_value,created_at | |
# ordering_params(params) # => { total_value: :desc, created_at: :asc } | |
# | |
# Usage: | |
# Order.order(ordering_params(params)) | |
ordering = {} | |
params[:sort].try(:split, ',').try(:each) do |attr| | |
attr = parse_attr attr | |
model = controller_name.titlecase.singularize.constantize | |
if model.attribute_names.include?(attr) | |
ordering[attr] = SORT_ORDER[parse_sign attr] | |
end | |
end | |
ordering | |
end | |
private | |
# Parsing of attributes to avoid empty starts in case browser passes "+" as " " | |
def parse_attr(attr) | |
'+' + attr.gsub(/^\ (.*)/, '\1') if attr.starts_with?(' ') | |
end | |
# Ordering sign parse, which separates | |
def parse_sign(attr) | |
attr =~ /\A[+-]/ ? attr.slice!(0) : '+' | |
end | |
end |
get '/:resource/', to: 'front_end#index', via: :get | |
get '/:resource/:id', to: 'front_end#show', via: :get | |
get '/:resource/:id/:nested/', to: 'front_end#nested_index', via: :get | |
match '/:resource/', to: 'front_end#schema', via: :options | |
match '/:resource/:id/:nested/', to: 'front_end#schema', via: :options |
Append and prepend are there to assure the expected execution order, is it a little overkilled?
For what I can see you tried to use @root_resource
and @nested_resource
to differentiate theirs uses.
But, I thought it a little confusing at first sight. First because set_resource
sets @resource
(and @root_resource), but set_nested
also sets @resource
.
What do you think about using only @resource
and changing the method names to set_root_resource
and set_nested_resource
?
Ooops... I think my suggestion don't apply. You set object from root_resource, then set nested_resource, from object.
Look at this diff. Perhaps a better approach (than my last suggestion).
https://gist.github.com/abinoam/762802f4f48e9d1aa98d48940b2fd724/revisions#diff-dd7bb1d0357534ad4ec0f31681fbe64b
Cool and (overall) clean code. 👏
I don't use append, prepend, at before_actions oftenly. What did you used them for? (I got curious about it).