Skip to content

Instantly share code, notes, and snippets.

@mjerem34
Created September 4, 2025 13:08
Show Gist options
  • Select an option

  • Save mjerem34/13eae595e7c3b13b12021ef2c8f8f081 to your computer and use it in GitHub Desktop.

Select an option

Save mjerem34/13eae595e7c3b13b12021ef2c8f8f081 to your computer and use it in GitHub Desktop.
JSON::API Spec Helpers
# app/controllers/api/v3/concerns/errors.rb
# frozen_string_literal: true
module Api
module V3
module Concerns
module Errors
extend ActiveSupport::Concern
included do
rescue_from ActiveRecord::RecordNotFound do |error|
render_json_errors(error.exception, 404)
end
rescue_from ActionController::ParameterMissing do |error|
render_json_errors(error.exception, 400)
end
end
end
end
end
end
# app/controllers/api/v3/concerns/filters.rb
# frozen_string_literal: true
module Api
module V3
module Concerns
module Filters
extend ActiveSupport::Concern
def filter(object)
return object unless params[:filter]
return object if action_name != 'index'
model_name = object.model.table_name
object.where(filters(model_name))
end
private
def filters(model_name)
filter_params.to_h.each do |key, value|
operator = value.split(':')[0]
build_sql_query(model_name, key, value, operator)
build_sql_values(value, operator)
end
return {} if sql_query.blank?
@filters ||= [sql_query.join(' AND '), *sql_values]
end
def build_sql_values(value, operator)
return if expected_value(value) == 'null'
return handle_sql_values_for_between_operator(value) if operator['between']
sql_values << expected_value(value).split(',')
end
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def build_sql_query(model_name, key, value, operator)
sql_query << equal_query(model_name, key, value) if operator['equals']
sql_query << not_equal_query(model_name, key, value) if operator['notEquals']
sql_query << "#{model_name}.#{key} > (?)" if operator['greaterThan']
sql_query << "#{model_name}.#{key} >= (?)" if operator['greaterOrEqual']
sql_query << "#{model_name}.#{key} < (?)" if operator['lessThan']
sql_query << "#{model_name}.#{key} <= (?)" if operator['lessOrEqual']
sql_query << "#{model_name}.#{key} BETWEEN (?) AND (?)" if operator['between']
sql_query << "#{model_name}.#{key} @> ARRAY[?]::varchar[]" if operator['contains']
end
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def handle_sql_values_for_between_operator(value)
expected_value(value).split(',').each { |v| sql_values << v }
end
def expected_value(value)
value.split(':')[1].gsub("'", '')
end
def equal_query(model_name, key, value)
operation = expected_value(value).downcase == 'null' ? 'IS NULL' : 'IN (?)'
"#{model_name}.#{key} #{operation}"
end
def not_equal_query(model_name, key, value)
operation = expected_value(value).downcase == 'null' ? 'IS NOT NULL' : 'NOT IN (?)'
"#{model_name}.#{key} #{operation}"
end
def sql_query
@sql_query ||= []
end
def sql_values
@sql_values ||= []
end
def filter_params
params.require(:filter).permit(allowed_filter_params)
end
def allowed_filter_params
[]
end
def formated_value(value)
return nil if value == 'nil'
value&.split(',')
end
end
end
end
end
# app/controllers/api/v3/lorems/ipsums_controller.rb
# frozen_string_literal: true
module Api
module V3
module Lorems
class IpsumsController < Api::V3::Lorems::BaseController
def index
render_serialized_json(lorem.impsums, serializer: ::V3::IpsumsSerializer)
end
private
def allowed_filter_params
%i[end_at start_at]
end
def allowed_order_params
%i[start_at]
end
end
end
end
end
# app/controllers/api/v3/concerns/order.rb
# frozen_string_literal: true
module Api
module V3
module Concerns
module Order
extend ActiveSupport::Concern
def order(object)
return object unless params[:sort]
return object if allowed_order_params.blank?
return object unless object.try(:length) && object.length > 1
object.reorder(order_method)
end
private
def order_method
order_params.select do |param|
param.inject(:merge).first.to_sym.in?(allowed_order_params)
end
end
def order_params
params.require(:sort).split(',').map do |param|
param.chr == '-' ? Hash[param.slice(1..-1), :desc] : Hash[param, :asc]
end
end
def allowed_order_params
[]
end
end
end
end
end
# app/controllers/api/v3/concerns/pagination.rb
# frozen_string_literal: true
module Api
module V3
module Concerns
module Pagination
include Pagy::Backend
extend ActiveSupport::Concern
def paginate(serializer, resources, options = {})
paginated_resources = page_all? ? resources : paginate_resources(resources)
meta_options = paginated_resources_meta(paginated_resources)
serializer.new(paginated_resources, meta_options.merge(options).with_indifferent_access)
end
private
def paginate_resources(resources)
per = per_param(resources)
if resources.is_a?(Array)
@pagy, @ressources = pagy_array(resources, page: page_param, items: per)
else
@pagy, @ressources = pagy(resources, page: page_param, items: per)
end
@ressources
end
def paginated_resources_meta(resources)
{
meta: {
current_page: @pagy&.page || 1,
next_page: @pagy&.next,
per_page: @pagy&.items || resources&.length,
prev_page: @pagy&.prev,
total_pages: @pagy&.pages || 1,
total_count: @pagy&.count || resources&.length
}
}
end
def page_all?
params[:page] == 'all'
end
def page_param
page_all? || params[:page].blank? ? 1 : params[:page]
end
def per_param(resources)
if page_all?
return resources.length.zero? ? 1 : resources.length
end
params[:per] || 25
end
end
end
end
end
# app/controllers/api/v3/concerns/response.rb
# frozen_string_literal: true
module Api
module V3
module Concerns
module Response
include ::Api::V3::Concerns::Filters
include ::Api::V3::Concerns::Order
extend ActiveSupport::Concern
def render_serialized_json(object, status = 200, serializer: nil, render_options: {})
filtered_object = filter(object)
ordered_filtered_object = order(filtered_object)
json = serialized_objects(ordered_filtered_object, render_options, serializer).to_json
render plain: json, content_type: 'application/json', status: status
end
def render_json_errors(errors, status = 400)
render plain: { errors: Array.wrap(errors) }.to_json,
content_type: 'application/json', status: status
end
private
def serializer_options
includes = params[:include] ? { include: params[:include].split(',') } : {}
fields = if params[:fields]
{ fields: params[:fields].permit!.to_h.transform_values { |v| v.split(',') } }
else
{}
end
{}.merge(includes, fields)
end
def serializer_klass(object_name)
_api, version, namespace, *_params = controller_path.split('/').map(&:classify)
serializer_name = [version, namespace, object_name].uniq.join('::')
"#{serializer_name}Serializer".constantize
end
def serializer_pagination(object, serializer, options)
if object.try(:size)
paginate(serializer, object, options)
else
serializer.new(object, options)
end
end
def serialized_objects(object, render_options, serializer = nil)
return {} if object.blank?
object_name = object.respond_to?(:each) ? object.first.class.name : object.class.name
serializer ||= serializer_klass(object_name)
options = serializer_options.merge(render_options)
serializer_pagination(object, serializer, options)
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment