What a busy week! New versions of dry-types and dry-validation have been released and there are really exciting new features awaiting for you. We're also very excited to see Trailblazer adopting some dry-rb libraries - Reform will soon support latest dry-validation, and Disposable has already replaced Virtus with dry-types for its coercion functionality.
New dry-validation features
The major highlight of the new 0.7.3 dry-validation release is improved integration with dry-types, a feature that we haven't revealed until now. Both dry-validation and dry-types are based on the same foundational library called dry-logic. Validation rules and type constraints are handled by dry-logic, which means we can use constrained types to share common constraints between type definitions and validation schemas to reduce information duplication. This will DRY-up your code (see what I did there?).
Here's what it means in practice:
require 'dry-validation'
require 'dry-types'
module Types
include Dry::Types.module
Age = Strict::Int.constrained(gt: 18)
end
UserSchema = Dry::Validation.Form do
key(:name).required
key(:age).required(Types::Age)
end
UserSchema.(name: 'Jane', age: 17).messages
# { age: ["must be greater than 18"] }
This way you can encapsulate important constraints and completely avoid duplication.
We are not stopping here!
Custom constructor types!
Another fantastic improvement is support for custom constructor types in validation schemas. You can now define types that can encapsulate custom coercion logic and dry-validation will infer an input processor from it and apply it to the input data before validation.
Simple example:
require 'dry-validation'
require 'dry-types'
module Types
include Dry::Types.module
module DataImport
StrippedString = Strict::String.constructor { |value| value.to_s.strip }
end
end
UserImport = Dry::Validation.Schema do
configure { config.input_processor = :sanitizer }
key(:name).required(Types::DataImport::StrippedString)
key(:email).required(Types::DataImport::StrippedString)
end
UserSchema.(name: ' Jane ', email: 'jane@doe.org ').to_h
# { name: "Jane", email: "jane@doe.org" }
This is a clean way of encapsulating custom coercion or sanitization logic. Notice that your type objects are small and reusable. You can also use them manually anywhere in your application, e.g. Types::DataImport::StrippedString[" foo "] # => "foo"
.
dry-validation infers both validation rules and value constructors from your types, notice that Types::DataImport::StrippedString
is a strict string, which means a proper validation rule is set too:
UserSchema.(name: ' Jane ', email: nil).messages
# { email: ["must be filled", "must be String"] }
Applying external schemas conditionally
When defining a high-level rule, you can now delegate validation to an external schema. Why would you want to do that? Let's say you have a nested data structure, and parts of that structure should be validated using different schemas based on other values from that data structure. In example:
CreateCommandSchema = Dry::Validation.Schema do
key(:name).required
key(:email).required
end
UpdateCommandSchema = Dry::Validation.Schema do
key(:id).required(:int?)
key(:data).schema(CreateCommandSchema)
end
CommandSchema = Dry::Validation.Schema do
key(:command).maybe(:str?, :inclusion?: %w(Create Update))
key(:args).maybe(:hash?)
rule(create_command: [:command, :args]) do |command, args|
command.eql?('Create').then(args.schema(CreateCommandSchema))
end
rule(update_command: [:command, :args]) do |command, args|
command.eql?('Update').then(args.schema(UpdateCommandSchema))
end
end
CommandSchema.(command: 'Oops').messages.inspect
# { command: ["must be one of: Create, Update"], args: ["is missing"] }
CommandSchema.(
command: 'Create', args: { name: 'Jane', email: nil }
).messages
# { args: { email: ["must be filled"] } }
CommandSchema.(
command: 'Update', args: { id: 1, data: { name: nil, email: 'jane@doe.org' } }
).messages
# { data: { name: ["must be filled"] } }
Notice that our high-level rules explicitly define which values they rely on. It means we don't have to worry about validation rules from external schemas crashing due to invalid state. If args
is nil
, nothing will crash. Furthermore, if command
is not one of the allowed values, we won't even bother trying to apply external schemas.
New predicate: :number?
You can now specify that a value must be a number, in case of Form
schemas a proper coercion is applied:
UserSchema = Dry::Validation.Form do
key(:age).required(:number?, :int?)
end
UserSchema.(age: '1').to_h
# { age: 1 }
UserSchema.(age: 'one').messages
# { age: ["must be a number"] }
UserSchema.(age: '1.5').messages
# { age: ["must be an integer"] }
New dry-types features
dry-types 0.7.0 focused on improving support for complex types and better exception messages. You can now specify complex types, like arrays or hashes, along with constraints.
Let's say you want to specify Options
type which is an array with either strings or string pairs inside nested arrays:
require 'dry-types'
module Types
include Dry::Types.module
StringList = Strict::Array.member(Strict::String)
StringPair = Strict::Array.member(Strict::String).constrained(size: 2)
StringPairs = Strict::Array.member(StringPair)
Options = StringList | StringPairs
end
Types::Options.call(["one", "two"])
# ["one", "two"]
Types::Options.call([["a", "one"], ["b", "two"]])
# [["a", "one"], ["b", "two"]]
Types::Options.call(["one", nil])
# Dry::Types::ConstraintError
Types::Options.call([["a", "one"], ["b"]])
# Dry::Types::ConstraintError
Exception messages have been improved too; however, they're still a work in progress, and complex types like in the previous example still need some work.
Here's what you'd get if a simple constraint was violated:
Password = Types::Strict::String.constrained(size: 10)
Password["foo"]
# Dry::Types::ConstraintError: "foo" violates constraints (size?(10) failed)
Enjoy!
We hope you'll find new features useful. In case of any issues, please report them on GitHub or chat with us on Zulip.
If you want to use new features, just add latest versions to your Gemfile:
gem "dry-types", "~> 0.7.0"
gem "dry-validation", "~> 0.7.3"
Detailed CHANGELOGs: