Cameron
Cameron

Reputation: 28853

Validate params in Rails

In my Rails app I want to validate the filter and post_type params.

Both are optional, but if they are present they must have a value and must have a value that matches one in an array of valid values.

In my controller I have two methods for checking them:

def validate_filter
  if params.has_key?(:filter)
    if params[:filter].present?
      if ['popular', 'following', 'picks', 'promoted', 'first-posts'].include?(params[:filter])
        return true
      else
        return false
      end
    else
      return false
    end
  else
    return true
  end
end

def validate_post_type
  if params.has_key?(:post_type)
    if params[:post_type].present?
      if ['discussions', 'snaps', 'code', 'links'].include?(params[:post_type])
        return true
      else
        return false
      end
    else
      return false
    end
  else
    return true
  end
end

And then in my main controller method I do:

def index
    raise ActionController::RoutingError.new('Not Found') unless validate_filter && validate_post_type
    ...

So this means post_type= and post_type=cam would return a 404 but post_type=snaps would return true.

Is there a better way to validate that the params passed are valid but for both empty and if the key itself exists. Using just blank? and present? is not enough in this scenario.

Upvotes: 3

Views: 28321

Answers (4)

Sapna Jindal
Sapna Jindal

Reputation: 422

You can do this in before_action callback of the controller.

before_action :validate_params, only: [:index]

def validate_params
  return false unless params[:filter].present? && params[:post_type].present?
   params_include?(:filter, %w(popular following picks promoted first-posts))
   params_include?(:post_type, %w(discussions snaps code links))
end

Upvotes: 4

Peter Toth
Peter Toth

Reputation: 1004

In a case of an API I would consider letting the client know, that there is a validation error rather than just saying 404.

How about using ActiveModel::Validations?

class MyParamsValidator
  include ActiveModel::Validations

  AVAILABLE_FILTERS    = %w(popular following picks promoted first-posts)
  # this might come from an enum like MyModel.post_types
  AVAILABLE_POST_TYPES = %w(discussions snaps code links)

  attr_reader :data

  validates :filter, inclusion: { in: AVAILABLE_FILTERS }, allow_blank: true
  validates :post_type, inclusion: { in: AVAILABLE_POST_TYPES }, allow_blank: true

  def initialize(data)
    @data = data
  end

  def read_attribute_for_validation(key)
    data[key]
  end
end

class MyController < ApplicationController
  before_action :validate_params, only: :index

  def validate_params
    validator = MyParamsValidator.new(params)

    return if validator.valid?

    render json: { errors: validator.errors }, status: 422
  end
end

You can find more info about a nested case with some tests here.

Upvotes: 6

spickermann
spickermann

Reputation: 107142

Perhaps a small helper method:

def validate_filter
  params_include?(:filter, %w(popular following picks promoted first-posts))
end

def validate_filter
  params_include?(:post_type, %w(discussions snaps code links))
end

def params_include?(key, values)
  !params.key?(key) || values.include?(params[key])
end

It is not clear from your question where that params are coming from, if they are query parameters or part of the path. If they are part of the path you might consider using routing constraints in your routes.rb

Upvotes: 2

user1875195
user1875195

Reputation: 988

I would probably move this logic to the model, but if you really want it in the controller you could simplify it.

def validate_filer
  return true unless params.has_key?(:filter)
  ['popular', 'following', 'picks', 'promoted', 'first-posts'].include?(params[:filter])
end

Upvotes: 5

Related Questions