dry-validation 0.8.0 released

After 2 months of hard work we are happy to announce the release of dry-validation 0.8.0! This release includes many new features, performance improvements and important bug fixes.

Upgrading

If you are upgrading from 0.7.x you should see plenty of deprecation warnings related to renamed macros and predicates. Updating it is pretty straightforward, but if you don’t feel like doing it now and warnings are annoying while you’re running your tests, just configure deprecation logger and revisit the output later:

Dry::Validation::Deprecations.configure do |config|
  config.logger = Logger.new(SPEC_ROOT.join('../log/deprecations.log'))
end

If you use customized error messages, you may have to update some of them as a couple of tokens have been renamed. Please see recent history of the built-in errors.yml for more details.

If you use custom predicates modules, there’s a new interface for configuring it and the old one no longer works:

Dry::Validation.Schema do
  configure { predicates(MyCustomPredicates) }
end

Please also notice that this release depends on latest dry-types and dry-logic.

Let’s take a look at a couple of highlights of this release.

New macros

We have refined existing macros and added a couple of new ones to allow rule compositions which were not possible before.

The new filled macro (which replaces required) and the maybe macro now support blocks too, and both are based on a simpler, new macro called value.

The key macro has been renamed to required which makes it easier to understand the difference between defining required or optional keys and rules for their values.

Here’s an example:

Dry::Validation.Schema do
  required(:name).filled(:str?, min_size?: 3)
  required(:age).filled(:int?, gt?: 18)
  optional(:phone_number).filled(:str?)
end

Now you can also use blocks:

Dry::Validation.Schema do
  required(:data).maybe(type?: Array) { size?(2) | size?(4) }
  required(:logs).value { type?(Array) | type?(Hash) }
end

This allows you to define each rules with additional rules for the value itself. For example let’s define a rule where the :data key is required and its value must be an array with 3 elements - where every element is an integer:

Dry::Validation.Schema do
  required(:data).value(type?: Array, min_size?: 3) { each(:int?) }
end

New predicates

A bunch of useful predicates have been added:

  • :included_in? when a value must be one of the specified values
  • :excluded_from? the opposite of :included_in?
  • :not_eql? when a value must not equal given value
  • :odd? and :even?

Here are some examples:

Dry::Validation.Schema do
  required(:tags).filled(:str?, included_in?: %w(red green blue))
  required(:num).filled(:int?, not_eql?: 10)
end

Customizable hints

In addition to a number of bug fixes, we’ve added support for defining a seperate message for hints and error messages. This allows you to customize messages when a value didn’t pass basic checks and you want to display additional messages that are different than errors:

To make use of this feature you need to tweak your errors.yml as follows:

en:
  errors:
    min_size?:
      failure: "size can't be less than %{num}"
      hint: "please make sure it has at least %{num} chars"

Now hints will be different than actual validation errors:

UserSchema = Dry::Validation.Schema do
  required(:login).filled(:str?, min_size?: 3)
end

UserSchema.(login: "").messages
# {:login=>["must be filled", "please make sure it has at least 3 chars"]}

UserSchema.(login: "fo").messages
{:login=>["size can't be less than 3"]}

Root-level rules

A root-level rule is applied to an input before any other rules in your schema. It’s a useful feature for cases where you can’t guarantee that the input will be the correct type (i.e. you don’t know if it will always be a hash).

Usage is very simple:

UserSchema = Dry::Validation.Schema do
  input :hash? # our root-level rule

  required(:name).filled(:str?)
end

UserSchema.(nil).messages # ["must be a hash"]

Notice that when a root-level rule fails, messages returns a flat array rather than a hash.

Support for zero-arity predicates

For context-aware schemas you can now define rules with predicates that rely on a schema’s state and don’t need any arguments.

Let’s say we want to make sure that :login_time exists for users that are logged in. We can verify this by checking if current_user[:id] exists using the following custom predicate:

UserSchema = Dry::Validation.Schema do
  configure do
    option :current_user, {}

    def current_user?
      current_user && current_user[:id]
    end
  end

  required(:login_time).maybe(:date_time?)

  rule(require_login_time: [:login_time]) do |login_time|
    current_user?.then(login_time.filled?)
  end
end

UserSchema.(name: "Jane", login_time: nil).messages
# {}

schema = UserSchema.with(current_user: { id: 1 })

schema.(name: "Jane", login_time: DateTime.now).messages
# {}

schema.(name: "Jane", login_time: nil).messages
# {:login_time=>["must be filled"]}

Improved messages

Messages now have access to all predicate arguments by default. If you add a custom predicate with arguments, the argument values will be available in your message templates using the name of the argument as the token.

For example:

en:
  errors:
    source_valid?: "my message has access to %{source} and %{target} :D"
UserSchema = Dry::Validation.Schema do
  configure do
    def source_valid?(source, target)
      false
    end
  end

  required(:data).value(source_valid?: "TADA")
end

UserSchema.(data: "w00t").messages
# {:data=>["my message has access to TADA and w00t :D"]}

New way of configuring coercions

The default behavior for coercions is to automatically infer them from rule definitions. It’s smart and reduces code duplication; however, it turned out to be extremely slow. Furthermore, it’s behaviour was a little too magical for our liking. That’s why we have decided to separate coercions from validation rules as of version 1.0.0. This release is the first step in that direction. The current API is not finalised so ideas and feedback are much appreciated!

For more details on this feature see our guide here.

Extendible DSL

You can now provide your own methods to DRY up your schema definitions:

module MyMacros
  def maybe_int(name, *predicates, &block)
    required(name).maybe(:int?, *predicates, &block)
  end
end

Dry::Validation::Schema.configure do |config|
  config.dsl_extensions = MyMacros
end

Dry::Validation.Schema do
  maybe_int(:age, gt?: 18)
end

Feel free to experiment with this and if you discover any common patterns let us know by reporting an issue. It might be a good candidate to be added to dry-validation!

…and more!

Yes, there’s more :) For detailed information about the changes and improvements please read the CHANGELOG.

Check out dry-validation and tell us what you think!