Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a DRY approach to applying a filter hash in a RAILS API controller index action?

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?

like image 805
begedin Avatar asked Jul 29 '15 08:07

begedin


People also ask

What is actioncontroller in Rails?

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.

What is before action in Rails?

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.


2 Answers

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
like image 114
tombeynon Avatar answered Oct 22 '22 18:10

tombeynon


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.

like image 2
linkyndy Avatar answered Oct 22 '22 19:10

linkyndy