valid , valid? , valid!

In this article, you’ll learn some interesting Ruby validation techniques. We’ll drive you trhough input and output validation inside ruby applications, the good and bads and some possible solutions.

You are probably already familiar with the different validation layers present in Rails (or other ruby frameworks). Viewing them from the request point of view we have something like:

validation-current

Rack validation gives you content type validation and some extra validations like files and body in multipart requests.

Rails versions >4 comes with strong params, which gives you the ability to define which of the input params are required or allowed, and with a not very exhaustive feature you can define the param type choosing between a short list of types.

Model validations in rails come from the `ActiveModel::Validations` module, they are awesome and you can do almost any kind of format, inclusion, uniqueness or custom validation there.

Aside from rails we also have:

What is missing

Let’s put it into perspective: if we draw the previous diagram matching sizes with how accurate the validation is in a regular Rails app we will have something like:

validation-perspective

This makes sense, as you are protecting the most precious attribute of your app, the persisted data. But let’s think about it from the performance and the security point of view: all kind of validation is good for your app health and the sooner you make it the sooner you will be able to stop the request processing, saving machine resources and adding previous layer security in your app.

So, why not move those sizes around and have a bigger param validation layer? why not include format, types, relations and all the power of the `ActiveModel::Validations` in the controller param validator layer?

This idea jumped into our minds during the new API architecture design, and we came up with something called ParamValidators. The idea is dead simple: use plain ruby objects with `ActiveModel::Validations` as service objects to validate and extract params before the controller uses them.

Param Validators

Let’s start with the base object handling the logic methods to build param validator ruby objects:

# app/validators/param_validator/base.rb
 
module ParamsValidator
  class Base
    include ActiveModel::Validations
    
    # Memoizes the accessor atrributes in the @attributes variable to be able
    # to list them and access them
    #
    def self.attr_accessor(*vars)
      @attributes ||= []
      @attributes.concat vars
      super(*vars)
    end
  
    # Accessor method to the memoized attrubutes setted by the attr_accessor method
    #
    # @return [Array(Symbols)]
    def self.attributes
      @attributes ||= []
 
      super_attributes = superclass.attributes if superclass && superclass.respond_to?(:attributes)
      (super_attributes || []) + @attributes
    end
    
    # Initializes the param validator object with the given controller params
    # HashwithIndifferentAccess object and feeds any defined attribute with the given param key
    # if they exists
    #
    def initialize(params = {})
      params.each do |key, value|
        attribute?(key) && send("#{key}=", value)
      end
    end
    
    # Alias for the attributes class method
    #
    # @return [Array(Symbol)]
    def attributes
      self.class.attributes
    end
    
    # Returns the given attibutes validated and parsed if needed
    # to be used in the controller
    # 
    # @return [HashWithIndifferentAccess]
    def to_params
      ::HashWithIndifferentAccess.new(attributes_hash)
    end
 
    private
    
    # Whenever the given they is a valid attribute or not
    #
    # @return [Boolean]
    def attribute?(key)
      self.class.instance_methods.include? key.to_sym
    end
 
    # Return the attributes of the validator filtered by
    # attributes has been set in the initializer with params
    #
    # @return [Array]
    def settled_attributes
      instance_vars = self.instance_variables.map do |attr|
        attr.to_s[1..-1].to_sym
      end
 
      instance_vars & attributes
    end
 
    # Return a hash with the attributes and values that
    # will be sent to the controller
    #
    # @return [Hash] {attribute1: value, attribute2: value}
    def attributes_hash
      settled_attributes.inject({}) do |out, attribute|
        value = public_send(attribute)
        out[attribute] = value
        out
      end
    end
  end
end

In this basic base object we take care of three things:

  • Collect the defined attributes with `attr_accessor` (allowed ones)
  • Initialize them with the given param keys
  • Provide a method to return a `HashWithIndifferentAccess` after assignation validation and parsing.

Now we are able to create objects like:

# app/validators/param_validator/fancy.rb
 
class ParamValidator::Fancy < ParamValidator::Base
  attr_accessor :user_id, :fancy_name, :fancy_description
  
  validates :user_id, :fancy_name, presence: true
  validates :user_id, integer: true
end


Here we are setting some allowed params with `attr_accessor`, some required ones with the presence validator and we could add any other param validator as format, size or type just using the regular `ActiveModel::Validation` methods or any other custom validation build in top of that.

But let’s see how we can use this in the controller. The first thing we built is the ParamsFor module allowing any controller including the module to easily use the param validator objects.


# lib/params_for.rb
 
require 'active_support/concern'
 
module ParamsFor
  extend ActiveSupport::Concern
 
  private
  
  # Strong params checker
  #
  # @param name [Symbol] camelcased validator class name
  # @param options [Hash] optional
  # @option options [Boolean] :class class of the validator
  # @return [Hash]
  def params_for(name, options = {})
    instance_var_name = "@#{name.to_s}_params"
    instance_var = instance_variable_get(instance_var_name)
    return instance_var if instance_var.present?
 
    if options[:class]
      validator_klass = options[:class]
    else
      validator_name = "ParamsValidator::#{name.to_s.classify}"
      validator_klass = validator_name.constantize
    end
 
    validator = validator_klass.new(params)
    
    unless validator.valid?
      render status: :bad_request, json: validator.errors.to_json and return 
    end
 
    instance_variable_set(instance_var_name, validator.to_params)
  end
end

This is just a meta function allowing you to instantiate a `ParamValidator` object with the current request params, validate it (rendering a :bad_request response with the json errors if is not valid) and memoizing the validated, filtered and parsed Hash. In short, it allows you to easily use `ParamValidator` objects in a controller just like this:

# app/controllers/fancy_controller.rb
 
class Fancycontroller < ApplicationController
  include ParamsFor
  
  # Creates a Fancy object by checking and validating params 
  # before that
  #
  def create
    ...
    @fancy = Fancy.new(fancy_params)  
    ...
  end
  
  protected
  
  # Strong params delegated to ParamValidator::Fancy
  # and memoized in @fancy_params var returned by this method
  #
  # @return [HashwithIndifferentAccess]
  def fancy_params
    params_for :fancy  
  end
end

Wrap up

In the last few paragraphs we explored the different validation types and responsibilities in rails applications, understood their use cases and detected the missing parts.

After that we proposed a partial solution to the problem by redifining the architectural way the input validation is built in current rails applications.

Links

We have an open source project here where you can participate or expose your ideas about the `ParamValidator` thing.

Thanks and happy coding.

 

andresbravog

 

Leave a Reply

Your email address will not be published. Required fields are marked *