Macros

This is an experimental feature and its API may change before 2.0.0

Macros are a simple way of streamlining rule definitions. Whenever you find yourself repeating the exact same type of validation rules, consider defining a macro to reduce duplication. You can either define globally available macros for all contracts, or a per-class macros where the class and its descandants will be able to access them.

Defining a global macro

To define a global macro you can use Dry::Validation.register_macro API. Here's a simple example where we define a macro that checks format of a string:

Dry::Validation.register_macro(:email_format) do
  unless /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.match?(value)
    key.failure('not a valid email format')
  end
end

Defining a macro for a contract class

Unlike global macros, contract macros are only available to the class and its descendants. To define a contract class macro you can use Dry::Validation::Contract.register_macro. Here's the same example like above but we'll define a class macro:

class ApplicationContract < Dry::Validation::Contract
  register_macro(:email_format) do
    unless /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.match?(value)
      key.failure('not a valid email format')
    end
  end
end

Using a macro with rules

To define a rule that will apply a macro, simply use Rule#validate method:

class NewUserContract < ApplicationContract
  params do
    required(:email).filled(:string)
  end

  rule(:email).validate(:email_format)
end

Using i18n backend with macros

If you want to provide a message for your macro, you need to set it under errors.%{your_macro} path in the messages yaml file. Using our example with :email_format macro, here's how it would look like:

en:
  dry_validation:
    errors:
      email_format: "not a valid email format"

Then the macro definition needs to use the same identifier when setting a failure message:

class ApplicationContract < Dry::Validation::Contract
  config.messages.backend = :i18n

  register_macro(:email_format) do
    unless /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.match?(value)
      key.failure(:email_format)
    end
  end
end

Macro with options

If you want a macro to be parameterized, you can achieve that by passing a hash with options where the keys are macro identifiers. This way you will have access to macro arguments in the rule block. Here's an example how we could define a macro that checks a minimum size of an array, where we pass the minimum size as an option.

First, let's define a :min_size macro. Notice that we use a macro parameter in the block, which is the instance of our macro that gives us access to its args:

class ApplicationContract < Dry::Validation::Contract
  register_macro(:min_size) do |macro:|
    min = macro.args[0]
    key.failure("must have at least #{min} elements") unless value.size >= min
  end
end

With the macro defined, you can simply use this with Rule#validate:

class NewUserContract < ApplicationContract
  params do
    required(:phone_numbers).value(:array)
  end

  rule(:phone_numbers).validate(min_size: 1)
end

octocatEdit on GitHub