Skip to content

Instantly share code, notes, and snippets.

@ErvalhouS
Last active July 10, 2018 15:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save ErvalhouS/31f62edb9ea704d3abf8269922ded732 to your computer and use it in GitHub Desktop.
Save ErvalhouS/31f62edb9ea704d3abf8269922ded732 to your computer and use it in GitHub Desktop.
A read-only frontend API abstraction applicable to any application.
# 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
@ErvalhouS
Copy link
Author

The only dependencies here are:

gem 'ransack'
gem 'will_paginate'

@abinoam
Copy link

abinoam commented Jul 9, 2018

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).

@ErvalhouS
Copy link
Author

Append and prepend are there to assure the expected execution order, is it a little overkilled? :shipit:

@abinoam
Copy link

abinoam commented Jul 9, 2018

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 ?

@abinoam
Copy link

abinoam commented Jul 9, 2018

Ooops... I think my suggestion don't apply. You set object from root_resource, then set nested_resource, from object.

@abinoam
Copy link

abinoam commented Jul 9, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment