hanami-validations/README.md

499 lines
13 KiB
Markdown
Raw Permalink Normal View History

2016-01-19 17:48:15 +00:00
# Hanami::Validations
2014-08-03 15:29:55 +00:00
2019-07-04 12:15:12 +00:00
Data validation library for Ruby
2014-08-06 07:22:08 +00:00
## Status
2018-07-10 12:01:27 +00:00
[![Gem Version](https://badge.fury.io/rb/hanami-validations.svg)](https://badge.fury.io/rb/hanami-validations)
2021-06-14 13:29:07 +00:00
[![CI](https://github.com/hanami/validations/workflows/ci/badge.svg?branch=main)](https://github.com/hanami/validations/actions?query=workflow%3Aci+branch%3Amain)
[![Test Coverage](https://codecov.io/gh/hanami/validations/branch/main/graph/badge.svg)](https://codecov.io/gh/hanami/validations)
2018-07-10 12:14:16 +00:00
[![Depfu](https://badges.depfu.com/badges/af6c6be539d9d587c7541ae7a013c9ff/overview.svg)](https://depfu.com/github/hanami/validations?project=Bundler)
2016-01-19 17:48:15 +00:00
[![Inline Docs](http://inch-ci.org/github/hanami/validations.svg)](http://inch-ci.org/github/hanami/validations)
2014-08-06 07:22:08 +00:00
2021-06-14 13:30:29 +00:00
## Version
**This branch contains the code for `hanami-validations` 2.x.**
2014-08-06 07:22:08 +00:00
## Contact
2016-01-19 17:48:15 +00:00
* Home page: http://hanamirb.org
* Community: http://hanamirb.org/community
* Guides: https://guides.hanamirb.org
2016-01-19 17:48:15 +00:00
* Mailing List: http://hanamirb.org/mailing-list
* API Doc: http://rdoc.info/gems/hanami-validations
* Bugs/Issues: https://github.com/hanami/validations/issues
* Support: http://stackoverflow.com/questions/tagged/hanami
* Chat: http://chat.hanamirb.org
2014-08-06 07:22:08 +00:00
## Rubies
__Hanami::Validations__ supports Ruby (MRI) 3.0+
2014-08-03 15:29:55 +00:00
## Installation
Add this line to your application's Gemfile:
```ruby
2019-07-04 12:15:12 +00:00
gem "hanami-validations"
2014-08-03 15:29:55 +00:00
```
And then execute:
2016-05-13 13:17:59 +00:00
```shell
$ bundle
```
2014-08-03 15:29:55 +00:00
Or install it yourself as:
2016-05-13 13:17:59 +00:00
```shell
$ gem install hanami-validations
```
2014-08-03 15:29:55 +00:00
## Usage
2019-07-04 12:15:12 +00:00
[Hanami](http://hanamirb.org), [ROM](https://rom-rb.org), and [DRY](https://dry-rb.org) projects are working together to create a strong Ruby ecosystem.
2021-08-09 07:38:24 +00:00
`hanami-validations` is based on [`dry-validation`](https://dry-rb.org/gems/dry-validation/master/), for this reason the documentation explains the basics of this gem, but for advanced topics, it links to `dry-validation` docs.
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
### Overview
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
The main object provided by this gem is `Hanami::Validator`.
It providers a powerful DSL to define a validation contract, which is made of a schema and optional rules.
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
A validation **schema** is a set of steps that filters, coerces, and checks the validity of incoming data.
Validation **rules** are a set of directives, to check if business rules are respected.
2019-07-04 12:15:12 +00:00
Only when the input is formally valid (according to the **schema**), validation **rules** are checked.
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
```ruby
# frozen_string_literal: true
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
require "hanami/validations"
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
class SignupValidator < Hanami::Validator
schema do
required(:email).value(:string)
required(:age).value(:integer)
end
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
rule(:age) do
key.failure("must be greater than 18") if value < 18
end
end
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
validator = SignupValidator.new
2019-07-04 12:15:12 +00:00
result = validator.call(email: "user@hanamirb.test", age: 37)
result.success? # => true
2019-07-04 12:15:12 +00:00
result = validator.call(email: "user@hanamirb.test", age: "foo")
result.success? # => false
result.errors.to_h # => {:age=>["must be an integer"]}
2019-07-04 12:15:12 +00:00
result = validator.call(email: "user@hanamirb.test", age: 17)
puts result.success? # => false
puts result.errors.to_h # => {:age=>["must be greater than 18"]}
```
2019-07-04 12:15:12 +00:00
### Schemas
2019-07-04 12:15:12 +00:00
A basic schema doesn't apply data coercion, input must already have the right Ruby types.
2016-05-13 13:17:59 +00:00
```ruby
2019-07-04 12:15:12 +00:00
# frozen_string_literal: true
2019-07-04 12:15:12 +00:00
require "hanami/validations"
2019-07-04 12:15:12 +00:00
class SignupValidator < Hanami::Validator
schema do
required(:email).value(:string)
required(:age).value(:integer)
end
end
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
validator = SignupValidator.new
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
result = validator.call(email: "user@hanamirb.test", age: 37)
puts result.success? # => true
2015-01-05 09:49:32 +00:00
2019-07-04 12:15:12 +00:00
result = validator.call(email: "user@hanamirb.test", age: "37")
puts result.success? # => false
puts result.errors.to_h # => {:age=>["must be an integer"]}
2016-05-13 13:17:59 +00:00
```
2015-01-05 09:49:32 +00:00
2019-07-04 12:15:12 +00:00
### Params
2015-01-05 09:49:32 +00:00
2019-07-04 12:15:12 +00:00
When used in _params mode_, a schema applies data coercion, before to run validation checks.
2015-01-05 09:49:32 +00:00
2019-07-04 12:15:12 +00:00
This is designed for Web form/HTTP params.
2016-05-13 13:17:59 +00:00
```ruby
2019-07-04 12:15:12 +00:00
# frozen_string_literal: true
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
require "bundler/setup"
require "hanami/validations"
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
class SignupValidator < Hanami::Validator
params do
required(:email).value(:string)
required(:age).value(:integer)
end
end
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
validator = SignupValidator.new
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
result = validator.call(email: "user@hanamirb.test", age: "37")
puts result.success? # => true
puts result.to_h # => {:email=>"user@hanamirb.test", :age=>37}
2016-05-13 13:17:59 +00:00
```
2019-07-04 12:15:12 +00:00
### JSON
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
When used in _JSON mode_, data coercions are still applied, but they follow different policies.
For instance, because JSON supports integers, strings won't be coerced into integers.
2016-05-13 13:17:59 +00:00
```ruby
2019-07-04 12:15:12 +00:00
# frozen_string_literal: true
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
require "hanami/validations"
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
class SignupValidator < Hanami::Validator
json do
required(:email).value(:string)
required(:age).value(:integer)
end
end
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
validator = SignupValidator.new
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
result = validator.call(email: "user@hanamirb.test", age: 37)
puts result.success? # => true
puts result.to_h # => {:email=>"user@hanamirb.test", :age=>37}
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
result = validator.call(email: "user@hanamirb.test", age: "37")
puts result.success? # => false
2016-05-13 13:17:59 +00:00
```
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
### Whitelisting
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
Unknown keys from incoming data are filtered out:
2014-10-20 15:57:58 +00:00
2016-05-13 13:17:59 +00:00
```ruby
2019-07-04 12:15:12 +00:00
# frozen_string_literal: true
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
require "hanami/validations"
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
class SignupValidator < Hanami::Validator
schema do
required(:email).value(:string)
end
end
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
validator = SignupValidator.new
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
result = validator.call(email: "user@hanamirb.test", foo: "bar")
puts result.success? # => true
puts result.to_h # => {:email=>"user@hanamirb.test"}
2016-05-13 13:17:59 +00:00
```
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
### Custom Types
2014-10-20 15:57:58 +00:00
```ruby
2019-07-04 12:15:12 +00:00
# frozen_string_literal: true
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
require "hanami/validations"
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
module Types
include Dry::Types()
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
StrippedString = Types::String.constructor(&:strip)
end
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
class SignupValidator < Hanami::Validator
params do
required(:email).value(Types::StrippedString)
required(:age).value(:integer)
end
end
2019-07-04 12:15:12 +00:00
validator = SignupValidator.new
2019-07-04 12:15:12 +00:00
result = validator.call(email: " user@hanamirb.test ", age: "37")
puts result.success? # => true
puts result.to_h # => {:email=>"user@hanamirb.test", :age=>37}
2014-10-20 15:57:58 +00:00
```
2019-07-04 12:15:12 +00:00
### Rules
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
Rules are performing a set of domain-specific validation checks.
Rules are executed only after the validations from the schema are satisfied.
2014-10-20 15:57:58 +00:00
```ruby
2019-07-04 12:15:12 +00:00
# frozen_string_literal: true
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
require "hanami/validations"
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
class EventValidator < Hanami::Validator
params do
required(:start_date).value(:date)
2016-05-13 13:17:59 +00:00
end
2019-07-04 12:15:12 +00:00
rule(:start_date) do
key.failure("must be in the future") if value <= Date.today
2016-05-13 13:17:59 +00:00
end
2014-10-20 15:57:58 +00:00
end
2019-07-04 12:15:12 +00:00
validator = EventValidator.new
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
result = validator.call(start_date: "foo")
puts result.success? # => false
puts result.errors.to_h # => {:start_date=>["must be a date"]}
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
result = validator.call(start_date: Date.today)
puts result.success? # => false
puts result.errors.to_h # => {:start_date=>["must be in the future"]}
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
result = validator.call(start_date: Date.today + 1)
puts result.success? # => true
puts result.to_h # => {:start_date=>#<Date: 2019-07-03 ((2458668j,0s,0n),+0s,2299161j)>}
```
2014-10-20 15:57:58 +00:00
2021-08-09 07:38:24 +00:00
Learn more about rules: https://dry-rb.org/gems/dry-validation/master/rules/
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
### Inheritance
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
Schema and rules validations can be inherited and used by subclasses
2014-10-20 15:57:58 +00:00
```ruby
2019-07-04 12:15:12 +00:00
# frozen_string_literal: true
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
require "hanami/validations"
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
class ApplicationValidator < Hanami::Validator
params do
optional(:_csrf_token).filled(:string)
2016-05-13 13:17:59 +00:00
end
2014-10-20 15:57:58 +00:00
end
2019-07-04 12:15:12 +00:00
class SignupValidator < ApplicationValidator
params do
required(:user).hash do
required(:email).filled(:string)
end
2016-05-13 13:17:59 +00:00
end
2014-10-20 15:57:58 +00:00
end
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
validator = SignupValidator.new
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
result = validator.call(user: { email: "user@hanamirb.test" }, _csrf_token: "abc123")
puts result.success? # => true
puts result.to_h # => {:user=>{:email=>"user@hanamirb.test"}, :_csrf_token=>"abc123"}
2014-10-20 15:57:58 +00:00
```
2019-07-04 12:15:12 +00:00
### Messages
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
Failure messages can be hardcoded or refer to a message template system.
`hanami-validations` supports natively a default YAML based message template system, or alternatively, `i18n` gem.
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
We have already seen rule failures set with hardcoded messages, here's an example of how to use keys to refer to interpolated messages.
2014-10-20 15:57:58 +00:00
```ruby
2019-07-04 12:15:12 +00:00
# frozen_string_literal: true
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
require "hanami/validations"
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
class ApplicationValidator < Hanami::Validator
config.messages.top_namespace = "bookshelf"
config.messages.load_paths << "config/errors.yml"
end
2014-10-20 15:57:58 +00:00
```
2019-07-04 12:15:12 +00:00
In the `ApplicationValidator` there is defined the application namespace (`"bookshelf"`), which is the root of the messages file.
Below that top name, there is the key `errors`. Everything that is nested here is accessible by the validations.
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
There are two ways to organize messages:
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
1. Right below `errors`. This is for **general purposes** error messages (e.g. `bookshelf` => `errors` => `taken`)
2. Below `errors` => `rules` => name of the attribute => custom key (e.g. `bookshelf` => `errors` => `age` => `invalid`). This is for **specific** messages that affect only a specific attribute.
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
Our **suggestion** is to start with **specific** messages and see if there is a need to generalize them.
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
```yaml
# config/errors.yml
en:
bookshelf:
errors:
taken: "oh noes, it's already taken"
network: "there is a network error (%{code})"
rules:
age:
invalid: "must be greater than 18"
email:
invalid: "not a valid email"
2016-05-13 13:17:59 +00:00
```
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
#### General purpose messages
2016-06-01 14:30:21 +00:00
```ruby
2019-07-04 12:15:12 +00:00
class SignupValidator < ApplicationValidator
schema do
required(:username).filled(:string)
2016-06-01 14:30:21 +00:00
end
2019-07-04 12:15:12 +00:00
rule(:username) do
key.failure(:taken) if values[:username] == "jodosha"
2016-05-13 13:17:59 +00:00
end
2014-10-20 15:57:58 +00:00
end
2019-07-04 12:15:12 +00:00
validator = SignupValidator.new
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
result = validator.call(username: "foo")
puts result.success? # => true
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
result = validator.call(username: "jodosha")
puts result.success? # => false
puts result.errors.to_h # => {:username=>["oh noes, it's already taken"]}
2014-10-20 15:57:58 +00:00
```
2019-07-04 12:15:12 +00:00
#### Specific messages
2014-10-20 15:57:58 +00:00
2019-07-04 12:15:12 +00:00
Please note that the failure key used it's the same for both the attributes (`:invalid`), but thanks to the nesting, the library is able to lookup the right message.
2014-10-20 15:57:58 +00:00
2016-05-13 13:17:59 +00:00
```ruby
2019-07-04 12:15:12 +00:00
class SignupValidator < ApplicationValidator
schema do
required(:email).filled(:string)
required(:age).filled(:integer)
2016-05-13 13:17:59 +00:00
end
2015-01-29 10:31:15 +00:00
2019-07-04 12:15:12 +00:00
rule(:email) do
key.failure(:invalid) unless values[:email] =~ /@/
2015-01-29 10:31:15 +00:00
end
2019-07-04 12:15:12 +00:00
rule(:age) do
key.failure(:invalid) if values[:age] < 18
2016-05-13 13:17:59 +00:00
end
2015-01-29 10:31:15 +00:00
end
2019-07-04 12:15:12 +00:00
validator = SignupValidator.new
2015-01-29 10:31:15 +00:00
2019-07-04 12:15:12 +00:00
result = validator.call(email: "foo", age: 17)
puts result.success? # => false
puts result.errors.to_h # => {:email=>["not a valid email"], :age=>["must be greater than 18"]}
2016-05-13 13:17:59 +00:00
```
2019-07-04 12:15:12 +00:00
#### Extra information
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
The interpolation mechanism, accepts extra, arbitrary information expressed as a `Hash` (e.g. `code: "123"`)
2016-05-13 13:17:59 +00:00
```ruby
2019-07-04 12:15:12 +00:00
class RefundValidator < ApplicationValidator
schema do
required(:refunded_code).filled(:string)
2016-05-13 13:17:59 +00:00
end
2019-07-04 12:15:12 +00:00
rule(:refunded_code) do
key.failure(:network, code: "123") if values[:refunded_code] == "error"
2016-05-13 13:17:59 +00:00
end
end
2019-07-04 12:15:12 +00:00
validator = RefundValidator.new
2019-07-04 12:15:12 +00:00
result = validator.call(refunded_code: "error")
puts result.success? # => false
puts result.errors.to_h # => {:refunded_code=>["there is a network error (123)"]}
2016-05-13 13:17:59 +00:00
```
2014-11-01 11:08:44 +00:00
2021-08-09 07:38:24 +00:00
Learn more about messages: https://dry-rb.org/gems/dry-validation/master/messages/
2019-07-04 12:15:12 +00:00
### External dependencies
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
If the validator needs to plug one or more objects to run the validations, there is a DSL to do so: `:option`.
When the validator is instantiated, the declared dependencies must be passed.
2014-11-01 11:08:44 +00:00
```ruby
2019-07-04 12:15:12 +00:00
# frozen_string_literal: true
2017-03-06 22:56:56 +00:00
2019-07-04 12:15:12 +00:00
require "hanami/validations"
2017-03-06 22:56:56 +00:00
2019-07-04 12:15:12 +00:00
class AddressValidator
def valid?(value)
value.match(/Rome/)
2016-05-13 13:17:59 +00:00
end
2014-10-20 15:57:58 +00:00
end
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
class DeliveryValidator < Hanami::Validator
option :address_validator
2019-07-04 12:15:12 +00:00
schema do
required(:address).filled(:string)
end
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
rule(:address) do
key.failure("not a valid address") unless address_validator.valid?(values[:address])
end
end
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
validator = DeliveryValidator.new(address_validator: AddressValidator.new)
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
result = validator.call(address: "foo")
puts result.success? # => false
puts result.errors.to_h # => {:address=>["not a valid address"]}
2016-05-13 13:17:59 +00:00
```
2021-08-09 07:38:24 +00:00
Read more about external dependencies: https://dry-rb.org/gems/dry-validation/master/external-dependencies/
2019-07-04 12:15:12 +00:00
### Mixin
2019-07-04 12:15:12 +00:00
`hanami-validations` 1.x used to ship a mixin `Hanami::Validations` to be included in classes to provide validation rules.
The 2.x series, still ships this mixin, but it will be probably removed in 3.x.
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
```ruby
# frozen_string_literal: true
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
require "hanami/validations"
2016-05-13 13:17:59 +00:00
2019-07-04 12:15:12 +00:00
class UserValidator
2016-01-19 17:48:15 +00:00
include Hanami::Validations
2019-07-04 12:15:12 +00:00
validations do
required(:number).filled(:integer, eql?: 23)
end
end
2019-07-04 12:15:12 +00:00
result = UserValidator.new(number: 23).validate
2019-07-04 12:15:12 +00:00
puts result.success? # => true
puts result.to_h # => {:number=>23}
puts result.errors.to_h # => {}
2019-07-04 12:15:12 +00:00
result = UserValidator.new(number: 11).validate
2019-07-04 12:15:12 +00:00
puts result.success? # => true
puts result.to_h # => {:number=>21}
puts result.errors.to_h # => {:number=>["must be equal to 23"]}
```
2016-05-13 13:17:59 +00:00
## FAQs
### Uniqueness Validation
Uniqueness validation isn't implemented by `Hanami::Validations` because the context of execution is completely decoupled from persistence.
Please remember that **uniqueness validation is a huge race condition between application and the database, and it doesn't guarantee records uniqueness for real.** To effectively enforce this policy you can use SQL database constraints.
Please read more at: [The Perils of Uniqueness Validations](http://robots.thoughtbot.com/the-perils-of-uniqueness-validations).
2019-07-04 12:15:12 +00:00
If you need to implement it, please use the External dependencies feature (see above).
2016-05-13 13:17:59 +00:00
2014-08-03 15:29:55 +00:00
## Contributing
1. Fork it ( https://github.com/hanami/validations/fork )
2014-08-03 15:29:55 +00:00
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request
2014-08-06 07:22:08 +00:00
## Copyright
2021-01-06 09:32:50 +00:00
Copyright © 2014-2021 Luca Guidi Released under MIT License
2016-01-22 11:35:10 +00:00
2016-01-19 17:48:15 +00:00
This project was formerly known as Lotus (`lotus-validations`).