Is there a DRY approach to applying filter hash on RAILS API controller index action?
As per the JSON API specification, we have to use a filter parameter filter to filter our records in the controller. In fact, the filter parameter is not actually set, but since it must contain multiple search criteria, it is obvious that a hash must be used.
The problem is that I often repeat myself in controller actions for different post types.
This is what it looks like for a filter that includes a list of IDs (to get a few specific entries).
def index
if params[:filter] and params[:filter][:id]
ids = params[:filter][:id].split(",").map(&:to_i)
videos = Video.find(ids)
else
videos = Video.all
end
render json: videos
end
For checking on nested properties, I think I could use fetch
or andand
, but it still doesn't look dry enough and I still do the same on different controllers.
Is there a way I could look better and not repeat myself so much?
source to share
Rather than using problems just to include the same code in multiple places, it seems like a good use for a service object.
class CollectionFilter
def initialize(filters={})
@filters = filters
end
def results
model_class.find(ids)
end
def ids
return [] unless @filters[:id]
@filters[:id].split(",").map(&:to_i)
end
def model_class
raise NotImplementedError
end
end
You can write generic CollectionFilter
as above and then subclass to add functionality for specific use cases.
class VideoFilter < CollectionFilter
def results
super.where(name: name)
end
def name
@filters[:name]
end
def model_class
Video
end
end
You would use this in your controller like below:
def index
videos = VideoFilter.new(params[:filter]).results
render json: videos
end
source to share
you can use Rails Concerns to dry it out ...
##================add common in app/models/concerns/common.rb
module Common
extend ActiveSupport::Concern
# included do
##add common scopes /validations
# end
##NOTE:add any instance method outside this module
module ClassMethods
def Find_using_filters (params)
Rails.logger.info "calling class method in concern=======#{params}=="
##Do whatever you want with params now
#you can even use switch case in case there are multiple models
end
end
end
##======================include the concern in model
include Common
##=======================in your controller,call it directly
Image.Find_using_filters params
source to share
Here's my example, somewhat adapted from Justin Weiss 's method :
# app/models/concerns/filterable.rb
module Filterable
extend ActiveSupport::Concern
class_methods do
def filter(params)
return self.all unless params.key? :filter
params[:filter].inject(self) do |query, (attribute, value)|
query.where(attribute.to_sym => value) if value.present?
end
end
end
end
# app/models/user.rb
class User < ActiveRecord::Base
include Filterable
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
# GET /users
# GET /users?filter[attribute]=value
def index
@users = User.filter(filter_params)
end
private
# Define which attributes can this model be filtered by
def filter_params
params.permit(filter: :username)
end
end
Then you filter the results by submitting GET /users?filter[username]=joe
. This works without filters (returns User.all
) or filters that don't matter (they're just skipped).
filter
must conform to JSON-API. With a model interest, you keep your DRY code and only include it in whatever models you want to filter. I also used strong parameters to provide some sort of protection from the "scary internet".
Of course, you can tweak this problem and make it support arrays as values ββfor filters.
source to share