dry-validation

repository·main·Indexed 21 days ago

https://github.com/dry-rb/dry-validation

A powerful data validation library for Ruby that allows the definition of complex validation rules and schemas to ensure data integrity. It provides support for different schema types (params, json, and plain schema), custom validation rules via the Evaluator API, and extensions for validation hints and predicates as macros. The library includes tools for localized error messages, global and contract-specific macros, and a dedicated interactive console for prototyping.

Tokens
4.3K
Snippets
13
Records
30
Agent score
80%

What's inside dry-validation

  1. Define validation rules with `rule` and `each`

    main

    In dry-validation, rules are used to perform complex validation logic that goes beyond simple schema checks. Rules are typically defined within a Contract.

    • rule: Used to define a rule for specific keys. You can pass macros to the rule to specify how it should behave.
    • each: Used to define a validation function that is applied to every element of an array. The each method can take macros as arguments. The validation function is only applied if the schema checks for the array item passed successfully.

    When a rule is executed, it creates an Evaluator instance. The block provided to the rule is evaluated within the context of this Evaluator, allowing you to use methods like key, value, and failure.

    # Example of using rule and each within a contract
    rule(:nums).each do |index:|
      key([:number, index]).failure("must be greater than 0") if value < 0
    end
    
    rule(:nums).each(min: 3)
    
    rule(address: :city) do
      key.failure("oops") if value != 'Munich'
    end
  2. Differentiate between #errors and #messages with hints enabled

    main

    When the :hints extension is active, the behavior of error-related methods changes:

    • #errors: Returns error messages but explicitly excludes hints (internally sets hints: false).
    • #messages: Returns both errors and hints combined into a single message set.
    • #hints: Returns only the hint messages.
  3. Use the Evaluator API within rule blocks

    main

    The Evaluator is the execution context for rules in dry-validation. When you define a rule in a contract, you are working within an instance of an Evaluator. It provides an API to track failures at specific paths and access the values being validated.

    Key capabilities include:

    • Accessing values: Use value to get the value of the current key, or key?(name) to check if a specific key exists in the input.
    • Reporting failures: Use key(path).failure(message) to attach an error to a specific path, or base.failure(message) for errors that don't belong to a specific key.
    • Checking existing errors: Use rule_error?(path) to see if there are already errors at a specific path.
    rule(:age) do
      key.failure(:invalid) if value < 18
    end
    
    # Or checking for key existence
    rule(:age) do
      key.failure(:invalid) if key? && value < 18
    end
    
    # Or specifying multiple keys
    rule(:start_date, :end_date) do
      if key?(:start_date) && !key?(:end_date)
        key(:end_date).failure("must provide an end_date with start_date")
      end
    end
  4. Inspect the Result object returned by Contracts

    main
    When you call a Contract#call, it returns a Dry::Validation::Result object. This object encapsulates the outcome of both the base schema validation and any custom rules defined in the contract. You can use it to check for success, retrieve processed values, or access error messages.
  5. Define a validation contract

    main

    To create a validation contract, inherit from Dry::Validation::Contract. You can define a schema (for data structure and coercion) and rules (for custom validation logic).

    There are three types of schemas you can define:

    1. params: Suitable for HTTP parameters (includes coercion).
    2. json: Suitable for JSON data.
    3. schema: A plain schema that does not offer coercion out of the box.

    Rules are defined using the rule method and can target specific keys or paths. If a rule targets a key not defined in the schema, an InvalidKeysError will be raised.

    class MyContract < Dry::Validation::Contract
      params do
        required(:name).filled(:string)
        required(:age).filled(:integer)
      end
    
      rule(:age) do
        failure('must be at least 18') if values[:age] < 18
      end
    end
  6. Use the dry-validation interactive console

    main

    The bin/console script provides an interactive IRB session pre-loaded with dry-validation and dry-types. It includes a Context helper that provides shorthand methods for common entry points, allowing you to quickly prototype schemas and contracts without manual boilerplate.

    Available helper methods in the console:

    • schema { ... }: Defines a Dry::Schema.
    • params { ... }: Defines a Dry::Schema.Params (handles stringified keys/coercion).
    • json { ... }: Defines a Dry::Schema.JSON (strict JSON types).
    • contract { ... }: Builds a Dry::Validation::Contract.
    • console: Re-enters the IRB session.
  7. Use predicates as macros in Contracts

    main

    By default, dry-validation contracts use predicates within schemas. You can enable the :predicates_as_macros extension to use specific predicates as macros within rule blocks. This allows you to perform validations like rule(:age).validate(gteq?: 18) instead of writing manual logic.

    To use this feature:

    1. Load the extension using Dry::Validation.load_extensions(:predicates_as_macros).
    2. Call import_predicates_as_macros in your base contract class.

    The available predicates for macros are: filled?, format?, gt?, gteq?, included_in?, includes?, inclusion?, is?, lt?, lteq?, max_size?, min_size?, not_eql?, odd?, respond_to?, size?, true?, and uuid_v4?.

    Dry::Validation.load_extensions(:predicates_as_macros)
    
    class ApplicationContract < Dry::Validation::Contract
      import_predicates_as_macros
    end
    
    class AgeContract < ApplicationContract
      schema do
        required(:age).filled(:integer)
      end
    
      rule(:age).validate(gteq?: 18)
    end
    
    AgeContract.new.(age: 17).errors.first.text
    # => 'must be greater than or equal to 18'
  8. Register a contract-specific macro

    main

    If you want a macro to be available only within a specific contract class, use register_macro inside that class definition. This keeps validation logic encapsulated and prevents polluting the global namespace.

    class MyContract < Dry::Validation::Contract
      register_macro(:even_numbers) do
        key.failure('all numbers must be even') unless values[key_name].all?(&:even?)
      end
    end
  9. Access validation hints using #hints

    main

    Once the :hints extension is loaded, the validation result object provides a #hints method. This returns a collection of hint messages associated with specific keys, allowing you to distinguish between structural errors and specific rule violations (hints).

    Dry::Validation.load_extensions(:hints)
    
    contract = Dry::Validation::Contract.build do
      schema do
        required(:name).filled(:string, min_size?: 2..4)
      end
    end
    
    # Returns hints specifically
    contract.call(name: "fo").hints
    # => {:name=>["size must be within 2 - 4"]}
    
    # Returns standard error messages
    contract.call(name: "").messages
    # => {:name=>["must be filled", "size must be within 2 - 4"]}