Elevando a validação em Rails com dry-validation: Uma abordagem elegante para regras de negócio complexas

Nota importante: Este post foca especificamente na implementação e nos benefícios da gem dry-validation para validações complexas. Os controllers e serviços mostrados são intencionalmente simplificados para destacar o uso dos contratos de validação. Em um ambiente de produção, você provavelmente implementaria padrões adicionais como Service Objects, Interactors ou outras abstrações para melhorar a organização do código e a separação de responsabilidades. O Desafio do Sistema de Eventos Imagine que você está construindo um sistema de gerenciamento de eventos para uma empresa que oferece diferentes tipos de celebrações: aniversários, eventos corporativos, casamentos, etc. Cada tipo de evento tem suas próprias regras e requisitos específicos. Por exemplo, para eventos de aniversário, a empresa só aceita reservas com mais de 10 pessoas, enquanto para eventos corporativos, o mínimo é de 5 pessoas. Além disso, eventos corporativos podem ter campos específicos como 'nome da empresa'. Ao desenvolver este sistema, você rapidamente percebe que as validações tradicionais do Rails começam a tornar seu modelo Event inchado e difícil de manter. Você precisa de uma solução que: Mantenha seu modelo limpo e focado Permita regras de validação específicas para cada tipo de evento Seja fácil de estender para adicionar novos tipos de eventos no futuro No meu post anterior, exploramos como os Form Objects podem resolver esse problema. Hoje, vamos dar um passo além e conhecer a gem dry-validation, uma abordagem mais declarativa e poderosa. Por que dry-validation? A gem dry-validation faz parte do ecossistema dry-rb, que traz conceitos de programação funcional para o Ruby. Com ela, podemos criar contratos de validação com sintaxe clara e expressiva, separando completamente as regras de validação dos nossos modelos e controllers. Algumas vantagens desta abordagem: Declarativa: As regras são expressas de forma clara e concisa Composição: Fácil reutilização de validações através de herança Flexibilidade: Validações contextuais e baseadas em múltiplos atributos Mensagens de erro estruturadas: Retorno de erros em formato estruturado Integração com tipos: Possibilidade de combinar com a gem dry-types Configurando o projeto Vamos usar o mesmo projeto de exemplo do post anterior, um sistema para reserva de eventos. Primeiro, adicionamos a gem ao Gemfile: # Gemfile gem 'dry-validation', '~> 1.10' gem 'dry-types', '~> 1.5' E executamos: bundle install Agora, vamos organizar adequadamente nosso código, criando as pastas necessárias: mkdir -p app/contracts/api/v1/events mkdir -p app/types mkdir -p app/services Definindo tipos personalizados com dry-types A integração com dry-types nos permite definir tipos personalizados com validações embutidas: # frozen_string_literal: true # app/types/types.rb require "dry-types" module Types include Dry.Types() # Tipos para número de pessoas específicos para cada tipo de evento BirthdayPeople = Integer.constrained(gt: 10) BusinessPeople = Integer.constrained(gt: 5) # Tipo específico para tipos de evento válidos EventType = String.enum("birthday", "business") end Implementando contratos com dry-validation Agora, vamos criar nossos contratos de validação: Contrato Base # frozen_string_literal: true # app/contracts/api/v1/events/base_contract.rb module Api module V1 module Events class BaseContract

Apr 1, 2025 - 21:11
 0
Elevando a validação em Rails com dry-validation: Uma abordagem elegante para regras de negócio complexas

Nota importante: Este post foca especificamente na implementação e nos benefícios da gem dry-validation para validações complexas. Os controllers e serviços mostrados são intencionalmente simplificados para destacar o uso dos contratos de validação. Em um ambiente de produção, você provavelmente implementaria padrões adicionais como Service Objects, Interactors ou outras abstrações para melhorar a organização do código e a separação de responsabilidades.

O Desafio do Sistema de Eventos

Imagine que você está construindo um sistema de gerenciamento de eventos para uma empresa que oferece diferentes tipos de celebrações: aniversários, eventos corporativos, casamentos, etc. Cada tipo de evento tem suas próprias regras e requisitos específicos.

Por exemplo, para eventos de aniversário, a empresa só aceita reservas com mais de 10 pessoas, enquanto para eventos corporativos, o mínimo é de 5 pessoas. Além disso, eventos corporativos podem ter campos específicos como 'nome da empresa'.

Ao desenvolver este sistema, você rapidamente percebe que as validações tradicionais do Rails começam a tornar seu modelo Event inchado e difícil de manter. Você precisa de uma solução que:

  1. Mantenha seu modelo limpo e focado
  2. Permita regras de validação específicas para cada tipo de evento
  3. Seja fácil de estender para adicionar novos tipos de eventos no futuro

No meu post anterior, exploramos como os Form Objects podem resolver esse problema. Hoje, vamos dar um passo além e conhecer a gem dry-validation, uma abordagem mais declarativa e poderosa.

Por que dry-validation?

A gem dry-validation faz parte do ecossistema dry-rb, que traz conceitos de programação funcional para o Ruby. Com ela, podemos criar contratos de validação com sintaxe clara e expressiva, separando completamente as regras de validação dos nossos modelos e controllers.

Algumas vantagens desta abordagem:

  • Declarativa: As regras são expressas de forma clara e concisa
  • Composição: Fácil reutilização de validações através de herança
  • Flexibilidade: Validações contextuais e baseadas em múltiplos atributos
  • Mensagens de erro estruturadas: Retorno de erros em formato estruturado
  • Integração com tipos: Possibilidade de combinar com a gem dry-types

Configurando o projeto

Vamos usar o mesmo projeto de exemplo do post anterior, um sistema para reserva de eventos. Primeiro, adicionamos a gem ao Gemfile:

# Gemfile
gem 'dry-validation', '~> 1.10'
gem 'dry-types', '~> 1.5'

E executamos:

bundle install

Agora, vamos organizar adequadamente nosso código, criando as pastas necessárias:

mkdir -p app/contracts/api/v1/events
mkdir -p app/types
mkdir -p app/services

Definindo tipos personalizados com dry-types

A integração com dry-types nos permite definir tipos personalizados com validações embutidas:

# frozen_string_literal: true

# app/types/types.rb
require "dry-types"

module Types
  include Dry.Types()

  # Tipos para número de pessoas específicos para cada tipo de evento
  BirthdayPeople = Integer.constrained(gt: 10)
  BusinessPeople = Integer.constrained(gt: 5)

  # Tipo específico para tipos de evento válidos
  EventType = String.enum("birthday", "business")
end

Implementando contratos com dry-validation

Agora, vamos criar nossos contratos de validação:

Contrato Base

# frozen_string_literal: true

# app/contracts/api/v1/events/base_contract.rb
module Api
  module V1
    module Events
      class BaseContract < Dry::Validation::Contract
        params do
          required(:title).filled(:string)
          required(:event_type).filled(::Types::EventType)
          required(:number_of_people).filled(:integer)
          optional(:description).maybe(:string)
          optional(:special_requests).maybe(:string)
        end
      end
    end
  end
end

Contrato para Eventos de Aniversário

# frozen_string_literal: true

module Api
  module V1
    module Events
      class BirthdayContract < BaseContract
        params do
          # mais campos podem ser adicionados aqui
        end

        rule(:number_of_people) do
          result = Types::BirthdayPeople.try(value)
          key.failure("must be greater than 10") if result.failure?
        rescue StandardError
          key.failure("is invalid")
        end
      end
    end
  end
end

Contrato para Eventos Corporativos

# frozen_string_literal: true

# app/contracts/api/v1/events/business_contract.rb
module Api
  module V1
    module Events
      class BusinessContract < BaseContract
        params do
          optional(:company_name).maybe(:string)
        end

        rule(:number_of_people) do
          result = Types::BusinessPeople.try(value)
          key.failure("must be greater than 5") if result.failure?
        rescue StandardError
          key.failure("is invalid")
        end
      end
    end
  end
end

Criando uma fábrica de contratos

Para selecionar o contrato correto com base no tipo de evento, implementamos uma factory:

# frozen_string_literal: true

class ContractFactory
  class InvalidEventTypeError < StandardError; end
  INVALID_EVENT_TYPE_MSG = "Invalid event type"

  def self.for(params)
    case params[:event_type]
    when Event.event_types[:birthday]
      ::Api::V1::Events::BirthdayContract.new
    when Event.event_types[:business]
      ::Api::V1::Events::BusinessContract.new
    else
      raise InvalidEventTypeError, INVALID_EVENT_TYPE_MSG
    end
  end
end

Implementando o controller

Nosso controller utiliza a factory para selecionar o contrato adequado:

# frozen_string_literal: true

module Api
  module V1
    class EventsController < ApplicationController
      def create
        begin
          contract = ::ContractFactory.for(event_params)
          result = contract.call(event_params)
        rescue ::ContractFactory::InvalidEventTypeError => e
          return render json: { error: e.message }, status: :bad_request
        end

        if result.success?
          create_event(result.to_h)
        else
          render json: { errors: result.errors.to_h }, status: :unprocessable_entity
        end
      end

      private

      def create_event(attributes)
        event = Event.new(attributes)
        if event.save
          render json: event, status: :created
        else
          render json: event.errors, status: :unprocessable_entity
        end
      end

      def event_params
        params.require(:event).permit(:title, :description, :event_type, :number_of_people, :special_requests, :company_name).to_h
      end
    end
  end
end

O modelo Event

Nosso modelo permanece simples, com apenas uma definição de enum para os tipos de evento:

# frozen_string_literal: true

class Event < ApplicationRecord
  enum :event_type, { business: "business", birthday: "birthday" }
end

Testando nossa implementação

Vamos usar cURL para testar nossa API:

Evento de aniversário (sucesso)

curl -X POST \
  http://localhost:3000/api/v1/events \
  -H 'Content-Type: application/json' \
  -d '{
    "event": {
      "title": "Festa de Aniversário de 30 anos",
      "description": "Celebração de aniversário com amigos e família",
      "event_type": "birthday",
      "number_of_people": 15,
      "special_requests": "Decoração em azul e branco"
    }
  }'

Resposta esperada (201 Created):

{
  "id": 1,
  "title": "Festa de Aniversário de 30 anos",
  "description": "Celebração de aniversário com amigos e família",
  "event_type": "birthday",
  "number_of_people": 15,
  "special_requests": "Decoração em azul e branco",
  "created_at": "2025-03-09T12:34:56.789Z",
  "updated_at": "2025-03-09T12:34:56.789Z"
}

Evento de aniversário (falha)

curl -X POST \
  http://localhost:3000/api/v1/events \
  -H 'Content-Type: application/json' \
  -d '{
    "event": {
      "title": "Pequena Festa de Aniversário",
      "description": "Celebração íntima de aniversário",
      "event_type": "birthday",
      "number_of_people": 8,
      "special_requests": "Bolo de chocolate"
    }
  }'

Resposta esperada (422 Unprocessable Entity):

{
  "errors": {
    "number_of_people": ["must be greater than 10"]
  }
}

Mais exemplos e cenários de teste podem ser encontrados no repositório do projeto. curls.txt ou curl_en.txt

Principais benefícios observados

Ao utilizar a abordagem com dry-validation e dry-types, obtivemos várias vantagens:

  1. Separação de responsabilidades: Validações são tratadas fora dos modelos, mantendo-os limpos
  2. Código declarativo: As regras de validação são expressas de forma clara e concisa
  3. Reutilização através de herança: A estrutura de contratos permite herdar validações comuns
  4. Validações específicas por tipo: Cada tipo de evento tem suas próprias regras
  5. Fácil extensibilidade: Adicionar novos tipos de eventos é simples e não afeta o código existente
  6. Validações baseadas em tipos: Integração com dry-types torna as validações ainda mais robustas

Comparação com outras abordagens

Característica Validações no Modelo Form Objects dry-validation
Separação de responsabilidades Baixa Alta Alta
Complexidade de implementação Baixa Média Média-Alta
Clareza das regras de validação Média Média Alta
Reutilização de código Baixa Média Alta
Específico por contexto Difícil Possível Fácil
Mensagens de erro estruturadas Não Depende Sim
Integração com tipagem Não Não Sim

Conclusão

A gem dry-validation oferece uma abordagem poderosa e flexível para lidar com validações complexas em aplicações Ruby on Rails. Especialmente em projetos com regras de negócio que variam conforme o contexto, esta técnica permite manter o código limpo, organizado e fácil de estender.

Embora exija um pouco mais de configuração inicial comparada às validações tradicionais do Rails, os benefícios se tornam evidentes à medida que o projeto cresce e as regras de validação se tornam mais complexas.

Vale ressaltar que as duas abordagens - Form Objects e dry-validation - podem coexistir e até se complementar em um projeto. A escolha depende das necessidades específicas, da complexidade das validações e das preferências da equipe.

O código completo deste exemplo está disponível no GitHub: event_reservation_system (branch: dry-validation)

Recursos adicionais

Sobre o Autor

Eu sou Rodrigo Nogueira trabalho com desenvolvimento desde 2011 e com Ruby on Rails desde 2015.

GitHub