According to the JSON API specification, we should use a filter query parmeter to filter our records in a controller. What the filter parameter actually is isn't really specified, but since it should be able to contain multiple criteria for searching, the obvious thing to do would be to use a hash.
The problem is, it seems like I'm repeating myself quite often in controller actions for different types of records.
Here's what things look like for just a filter that includes a list of ids (to get multiple specific records).
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 nested property checks, I guess I could use fetch
or andand
but it still doesn't look dry enough and I'm still doing the same thing across different controllers.
Is there a way I could make this look better and not repeat myself that much?
A controller can thus be thought of as a middleman between models and views. It makes the model data available to the view, so it can display that data to the user, and it saves or updates user data to the model. For more details on the routing process, see Rails Routing from the Outside In.
When writing controllers in Ruby on rails, using before_action (used to be called before_filter in earlier versions) is your bread-and-butter for structuring your business logic in a useful way. It's what you want to use to "prepare" the data necessary before the action executes.
Rather than using concerns to just include the same code in multiple places, this 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 could write a generic CollectionFilter
as above, 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 as below;
def index
videos = VideoFilter.new(params[:filter]).results
render json: videos
end
Here is my take on this, somewhat adapted from Justin Weiss' 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
You would then filter the results by issuing a GET /users?filter[username]=joe
. This works with no filters (returns User.all
) or filters that have no value (they are simply skipped) also.
The filter
is there to comply with JSON-API. By having a model concern you keep your code DRY and only include it in whatever models you want to filter. I've also used strong params to enforce some kind protection against "the scary internet".
Of course you can customize this concern and make it support arrays as values for filters.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With