dry-validation

History

Rails 3

mass-assignment protection

# == Schema Information
#
# Table name: users
#
#  id            :integer          not null, primary key
#  email         :string
#  name          :string
#  admin         :boolean          default(FALSE)
#

class User < ActiveRecor::Base
  attr_accessible :email, :name
end

class UsersController < ApplicationController
  def create
    # { "user" => { "email" => "user@example.com", "name" => "Jon Snow", "admin" => true }
    User.create(params[:user])
  end
end
  
# == Schema Information
#
# Table name: users
#
#  id            :integer          not null, primary key
#  email         :string
#  name          :string
#  admin         :boolean          default(FALSE)
#

class User < ActiveRecor::Base
  attr_accessible :email, :name
  attr_accessible :email, :name, :admin, as: :admin
end

class Admin::UsersController < ApplicationController
  def create
    # { "user" => { "email" => "user@example.com", "name" => "Jon Snow", "admin" => true }
    User.create(params[:user], as: :admin)
  end
end
  
irb(main):001:0> user = User.create(email: "user@example.com", name: "Jon Snow", admin: true)
irb(main):002:0> user.admin
false

FormObject for conditional changes

2011 - Virtus

Rails 4

strong parameters

class PeopleController < ActionController::Base
  # This will raise an ActiveModel::ForbiddenAttributes exception
  # because it's using mass assignment without an explicit permit step.
  def create
    Person.create(params[:person])
  end

  # This will pass with flying colors as long as there's a person key
  # in the parameters, otherwise it'll raise an ActionController::ParameterMissing
  # exception, which will get caught by ActionController::Base and turned 
  # into that 400 Bad Request reply.
  def update
    person = current_account.people.find(params[:id])
    person.update_attributes!(person_params)
    redirect_to person
  end

  private
    # Using a private method to encapsulate the permissible parameters
    # is just a good pattern since you'll be able to reuse the same permit 
    # list between create and update. Also, you can specialize this method
    # with per-user checking of permissible attributes.
    def person_params
      params.require(:person).permit(:name, :age)
    end
end
# nested attributes

params.permit(:name, {emails: []}, friends: [:name, { family: [:name], hobbies: []}])

# outside controller

raw_parameters = { :email => "john@example.com", :name => "John", :admin => true }
parameters = ActionController::Parameters.new(raw_parameters)
user = User.create(parameters.permit(:name, :email))

Issues

  • Data type validation
  • Hard or impossible to define complex rules
  • Coupled with ActiveRecord
  • ...

dry-validation

class MyContract < Dry::Validation::Contract
  params do
    required(:id).filled(:integer)
    required(:name).filled(:string)
  end
end

MyContract.new.call("id" => 1, "name" => "Jon Snow")
class MyContract < Dry::Validation::Contract
  params do
    required(:id).filled(:integer)
    required(:name).filled(:string)
  end
end

MyContract.new.call("id" => 1, "name" => "Jon Snow")
module Types
  include Dry.Types

  Multiline = Types::Array.of(Types::String).constructor { |v|
    v.split(/\r?\n/)
  }
end

class MyContract < Dry::Validation::Contract
  params do
    required(:textarea).filled(Types::Multiline)
  end
end

# ideas: UUID, ID, CountryCode, PostCode
Dry::Validation.Contract do
  register_macro(:qux) do
    if values[:bar]
      key.failure("do not repeat bar") if value == values[:bar]
    end
  end

  params do
    optional(:foo).hash do
      required(:bar).value(:integer)
      required(:baz).maybe(:string)
    end
  end

  rule("foo.bar") do
    key.failed("invalid") if value > 3
  end

  rule("foo.baz").validate(:qux)
end
Dry::Validation.Contract do
  params do
    required(:foo).optional(:string)
    required(:bar).optional(:string)
  end

  rule do
    if values[:foo].blank? && values[:bar].blank?
      base.failure("provide at least foo or bar")
    end
  end
end

Rails + dry-validation

gem "gate", "~> 1.0"

class MyController < ApplicationController
  include Gate::Rails

  before_action :verify_contract

  contract(handler: :method_to_handle_error) do
    params do
      required(:name).filled(:string)
    end
  end
  def create
    claimed_params[:name]
    # ...
  end
end

Questions?

Made with Slides.com