Medium : Rails : nested routes, polymorphic associations and controllers (https://medium.com/@loickartono/rails-nested-routes-polymorphic-associations-and-controllers-8ade7249fa49)
module Behaveable
module RouteExtractor
# Generate url location.
#
# ==== Parameters
# * <tt>behaveable</tt> - Behaveable object.
# * <tt>resource</tt> - Resource object. (member routes).
#
# ==== Returns
# * <tt>Route</tt> - Url location.
def extract(behaveable = nil, resource = nil)
resource_name = resource_name_from(params)
behaveable_name = behaveable_name_from(behaveable)
location_url = "#{resource_name}_url"
return regular(location_url, resource) unless behaveable
location_url = "#{behaveable_name}_#{resource_name}_url"
nested(location_url, behaveable, resource)
end
private
# Handle non-nested url location.
#
# ==== Parameters
# * <tt>location_url</tt> - Url route as string.
# * <tt>resource</tt> - Resource object.
#
# ==== Returns
# * <tt>Route</tt> - Url location.
def regular(location_url, resource)
return send(location_url) unless resource
send(location_url, resource)
end
# Handle nested url location.
#
# ==== Parameters
# * <tt>location_url</tt> - Url route as string.
# * <tt>behaveable</tt> - Behaveable object.
# * <tt>resource</tt> - Resource object.
#
# ==== Returns
# * <tt>Route</tt> - Url location.
def nested(location_url, behaveable, resource)
return send(location_url, behaveable) unless resource
send(location_url, behaveable, resource)
end
# Get resource name from params.
#
# ==== Parameters
# * <tt>params</tt> - ApplicationController's params.
#
# ==== Returns
# * <tt>String</tt> - Resource name (singular or plural).
def resource_name_from(params)
inflection = params[:id].present? ? 'singular' : 'plural'
params[:controller].split('/').last.send("#{inflection}ize")
end
# Get behaveable class name.
#
# ==== Parameters
# * <tt>behaveable</tt> - Behaveable object.
#
# ==== Returns
# * <tt>String</tt> - Behaveable class snake case name or nil.
def behaveable_name_from(behaveable)
return unless behaveable
behaveable.class.name.underscore
end
end
end
module Behaveable
module ResourceFinder
# Get the behaveable object.
#
# ==== Returns
# * <tt>ActiveRecord::Model</tt> - Behaveable instance object.
def behaveable
klass, param = behaveable_class
klass.find(params[param.to_sym]) if klass
end
private
# Lookup behaveable class.
#
# ==== Returns
# * <tt>Response</tt> - Behaveable class object or nil if not found.
def behaveable_class
params.each do |name, _value|
if name =~ /(.+)_id$/
model = name.match(%r{([^\/.]*)_id$})
return model[1].classify.constantize, name
end
end
nil
end
end
end
class CategoriesController < ApplicationController
include Behaveable::ResourceFinder
include Behaveable::RouteExtractor
# Response type.
respond_to :json
# Get categories.
#
# GET (/:categorizable/:categorizable_id)/categories(.:format)
#
# ==== Returns
# * <tt>Response</tt> - JSON serialized categories.
def index
categories = categorizable.all
respond_with categories, status: :ok, location: extract(@behaveable)
end
# Get a category.
#
# GET (/:categorizable/:categorizable_id)/categories/:id(.:format)
#
# ==== Returns
# * <tt>Response</tt> - JSON serialized category.
def show
category = categorizable.find(params[:id])
respond_with category, status: :ok, location: extract(@behaveable, category)
end
# Create a category.
#
# POST (/:categorizable/:categorizable_id)/categories(.:format)
#
# ==== Returns
# * <tt>Response</tt> - JSON serialized category or errors if any.
def create
category = categorizable.new(category_params)
respond_to do |format|
category.transaction do
if category.save
categorizable << category if @behaveable
format.json { render json: category, status: :created }
else
format.json { render errors_for(category) }
end
end
end
end
# Update a category.
#
# PATCH (/:categorizable/:categorizable_id)/categories/:id(.:format)
#
# ==== Returns
# * <tt>Response</tt> - JSON serialized category or errors if any.
def update
category = categorizable.find(params[:id])
respond_to do |format|
if category.update(category_params)
format.json { render json: category, status: :ok }
else
format.json { render errors_for(category) }
end
end
end
# Delete a category.
#
# DELETE (/:categorizable/:categorizable_id)/categories/:id(.:format)
#
# ==== Returns
# * <tt>Response</tt> - 204 no content.
def destroy
category = categorizable.find(params[:id])
category.destroy if category
respond_to do |format|
format.json { head :no_content }
end
end
private
# Get Category context object.
#
# ==== Returns
# * <tt>ActiveRecord</tt> - Categorizable's categories or Category.
def categorizable
@behaveable ||= behaveable
@behaveable ? @behaveable.categories : Category
end
# ActiveRecord object errors.
# TODO: Should be placed at ApplicationController level ??.
#
# ==== Parameters
# * <tt>object</tt> - ActiveRecord object.
#
# ==== Returns
# * <tt>Hash</tt> - Hash containing object errors if any.
def errors_for(object)
{ json: { errors: object.errors }, status: :unprocessable_entity }
end
# Sanitize request data.
#
# ==== Returns
# * <tt>Hash</tt> - Sanitized request params.
def category_params
params.require(:category).permit(:name)
end
end