Skip to content

Instantly share code, notes, and snippets.

@softmonkeyjapan
Last active February 5, 2023 23:47
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save softmonkeyjapan/6143ff56b25df2d6fd1c to your computer and use it in GitHub Desktop.
Save softmonkeyjapan/6143ff56b25df2d6fd1c to your computer and use it in GitHub Desktop.
Medium : Rails : nested routes, polymorphic associations and controllers (https://medium.com/@loickartono/rails-nested-routes-polymorphic-associations-and-controllers-8ade7249fa49)
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
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
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
@markmcdonald51
Copy link

Hi!

This is really cool and seems really useful for a bunch of things I am working on. After messing with it work a little while it looks like you forgot to add a '+' in your regex capture see below:

original:
[3] pry(#)> model = name.match(%r{([^\/.])_id$}) if name =~ /(.+)_id$/
=> #<MatchData "n_id" 1:"n">

with plus sign: %r{([^\/.]+)_id$}

[5] pry(#)> model = name.match(%r{([^\/.]+)_id$}) if name =~ /(.+)_id$/
=> #<MatchData "organization_id" 1:"organization">

Now I am just wondering what is the resource_url can use that will reflect the proper polymorphic reflection?
I can get it to work by using a routed url like:

form_for @project, url: organization_projects_path do |f|

but I must be doing something wrong because it seems like this should be something like resource_url ?

Thanks in advance for this cool post! Definitely like to make use of it.

Cheers,
Mark

@cdesch
Copy link

cdesch commented Jun 28, 2018

@softmonkeyjapan I get this error in my History Controller when implementing your example (swapping category for history)

Unable to autoload constant Behaveable::ResourceFinder, expected /Users/cj/RubymineProjects/ad_rfp/app/controllers/concerns/behaveable/resource_finder.rb to define it

here is the history controller:

class HistoryController < ApplicationController

  include Behaveable::ResourceFinder
  include Behaveable::RouteExtractor

  def history
    # @behaveable ||= behaveable
    # @behaveable ? @behaveable.versions : PaperTrail::Version
  end
end

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