# Hanami::Validations Data validation library for Ruby ## Status [![Gem Version](https://badge.fury.io/rb/hanami-validations.svg)](https://badge.fury.io/rb/hanami-validations) [![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) [![Depfu](https://badges.depfu.com/badges/af6c6be539d9d587c7541ae7a013c9ff/overview.svg)](https://depfu.com/github/hanami/validations?project=Bundler) [![Inline Docs](http://inch-ci.org/github/hanami/validations.svg)](http://inch-ci.org/github/hanami/validations) ## Version **This branch contains the code for `hanami-validations` 2.x.** ## Contact * Home page: http://hanamirb.org * Community: http://hanamirb.org/community * Guides: https://guides.hanamirb.org * 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 ## Rubies __Hanami::Validations__ supports Ruby (MRI) 3.0+ ## Installation Add this line to your application's Gemfile: ```ruby gem "hanami-validations" ``` And then execute: ```shell $ bundle ``` Or install it yourself as: ```shell $ gem install hanami-validations ``` ## Usage [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. `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. ### Overview 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. 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. Only when the input is formally valid (according to the **schema**), validation **rules** are checked. ```ruby # frozen_string_literal: true require "hanami/validations" class SignupValidator < Hanami::Validator schema do required(:email).value(:string) required(:age).value(:integer) end rule(:age) do key.failure("must be greater than 18") if value < 18 end end validator = SignupValidator.new result = validator.call(email: "user@hanamirb.test", age: 37) result.success? # => true result = validator.call(email: "user@hanamirb.test", age: "foo") result.success? # => false result.errors.to_h # => {:age=>["must be an integer"]} result = validator.call(email: "user@hanamirb.test", age: 17) puts result.success? # => false puts result.errors.to_h # => {:age=>["must be greater than 18"]} ``` ### Schemas A basic schema doesn't apply data coercion, input must already have the right Ruby types. ```ruby # frozen_string_literal: true require "hanami/validations" class SignupValidator < Hanami::Validator schema do required(:email).value(:string) required(:age).value(:integer) end end validator = SignupValidator.new result = validator.call(email: "user@hanamirb.test", age: 37) puts result.success? # => true result = validator.call(email: "user@hanamirb.test", age: "37") puts result.success? # => false puts result.errors.to_h # => {:age=>["must be an integer"]} ``` ### Params When used in _params mode_, a schema applies data coercion, before to run validation checks. This is designed for Web form/HTTP params. ```ruby # frozen_string_literal: true require "bundler/setup" require "hanami/validations" class SignupValidator < Hanami::Validator params do required(:email).value(:string) required(:age).value(:integer) end end validator = SignupValidator.new result = validator.call(email: "user@hanamirb.test", age: "37") puts result.success? # => true puts result.to_h # => {:email=>"user@hanamirb.test", :age=>37} ``` ### JSON 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. ```ruby # frozen_string_literal: true require "hanami/validations" class SignupValidator < Hanami::Validator json do required(:email).value(:string) required(:age).value(:integer) end end validator = SignupValidator.new result = validator.call(email: "user@hanamirb.test", age: 37) puts result.success? # => true puts result.to_h # => {:email=>"user@hanamirb.test", :age=>37} result = validator.call(email: "user@hanamirb.test", age: "37") puts result.success? # => false ``` ### Whitelisting Unknown keys from incoming data are filtered out: ```ruby # frozen_string_literal: true require "hanami/validations" class SignupValidator < Hanami::Validator schema do required(:email).value(:string) end end validator = SignupValidator.new result = validator.call(email: "user@hanamirb.test", foo: "bar") puts result.success? # => true puts result.to_h # => {:email=>"user@hanamirb.test"} ``` ### Custom Types ```ruby # frozen_string_literal: true require "hanami/validations" module Types include Dry::Types() StrippedString = Types::String.constructor(&:strip) end class SignupValidator < Hanami::Validator params do required(:email).value(Types::StrippedString) required(:age).value(:integer) end end validator = SignupValidator.new result = validator.call(email: " user@hanamirb.test ", age: "37") puts result.success? # => true puts result.to_h # => {:email=>"user@hanamirb.test", :age=>37} ``` ### Rules Rules are performing a set of domain-specific validation checks. Rules are executed only after the validations from the schema are satisfied. ```ruby # frozen_string_literal: true require "hanami/validations" class EventValidator < Hanami::Validator params do required(:start_date).value(:date) end rule(:start_date) do key.failure("must be in the future") if value <= Date.today end end validator = EventValidator.new result = validator.call(start_date: "foo") puts result.success? # => false puts result.errors.to_h # => {:start_date=>["must be a date"]} result = validator.call(start_date: Date.today) puts result.success? # => false puts result.errors.to_h # => {:start_date=>["must be in the future"]} result = validator.call(start_date: Date.today + 1) puts result.success? # => true puts result.to_h # => {:start_date=>#} ``` Learn more about rules: https://dry-rb.org/gems/dry-validation/master/rules/ ### Inheritance Schema and rules validations can be inherited and used by subclasses ```ruby # frozen_string_literal: true require "hanami/validations" class ApplicationValidator < Hanami::Validator params do optional(:_csrf_token).filled(:string) end end class SignupValidator < ApplicationValidator params do required(:user).hash do required(:email).filled(:string) end end end validator = SignupValidator.new 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"} ``` ### Messages 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. We have already seen rule failures set with hardcoded messages, here's an example of how to use keys to refer to interpolated messages. ```ruby # frozen_string_literal: true require "hanami/validations" class ApplicationValidator < Hanami::Validator config.messages.top_namespace = "bookshelf" config.messages.load_paths << "config/errors.yml" end ``` 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. There are two ways to organize messages: 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. Our **suggestion** is to start with **specific** messages and see if there is a need to generalize them. ```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" ``` #### General purpose messages ```ruby class SignupValidator < ApplicationValidator schema do required(:username).filled(:string) end rule(:username) do key.failure(:taken) if values[:username] == "jodosha" end end validator = SignupValidator.new result = validator.call(username: "foo") puts result.success? # => true result = validator.call(username: "jodosha") puts result.success? # => false puts result.errors.to_h # => {:username=>["oh noes, it's already taken"]} ``` #### Specific messages 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. ```ruby class SignupValidator < ApplicationValidator schema do required(:email).filled(:string) required(:age).filled(:integer) end rule(:email) do key.failure(:invalid) unless values[:email] =~ /@/ end rule(:age) do key.failure(:invalid) if values[:age] < 18 end end validator = SignupValidator.new 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"]} ``` #### Extra information The interpolation mechanism, accepts extra, arbitrary information expressed as a `Hash` (e.g. `code: "123"`) ```ruby class RefundValidator < ApplicationValidator schema do required(:refunded_code).filled(:string) end rule(:refunded_code) do key.failure(:network, code: "123") if values[:refunded_code] == "error" end end validator = RefundValidator.new result = validator.call(refunded_code: "error") puts result.success? # => false puts result.errors.to_h # => {:refunded_code=>["there is a network error (123)"]} ``` Learn more about messages: https://dry-rb.org/gems/dry-validation/master/messages/ ### External dependencies 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. ```ruby # frozen_string_literal: true require "hanami/validations" class AddressValidator def valid?(value) value.match(/Rome/) end end class DeliveryValidator < Hanami::Validator option :address_validator schema do required(:address).filled(:string) end rule(:address) do key.failure("not a valid address") unless address_validator.valid?(values[:address]) end end validator = DeliveryValidator.new(address_validator: AddressValidator.new) result = validator.call(address: "foo") puts result.success? # => false puts result.errors.to_h # => {:address=>["not a valid address"]} ``` Read more about external dependencies: https://dry-rb.org/gems/dry-validation/master/external-dependencies/ ### Mixin `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. ```ruby # frozen_string_literal: true require "hanami/validations" class UserValidator include Hanami::Validations validations do required(:number).filled(:integer, eql?: 23) end end result = UserValidator.new(number: 23).validate puts result.success? # => true puts result.to_h # => {:number=>23} puts result.errors.to_h # => {} result = UserValidator.new(number: 11).validate puts result.success? # => true puts result.to_h # => {:number=>21} puts result.errors.to_h # => {:number=>["must be equal to 23"]} ``` ## 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). If you need to implement it, please use the External dependencies feature (see above). ## Contributing 1. Fork it ( https://github.com/hanami/validations/fork ) 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 ## Copyright Copyright © 2014-2021 Luca Guidi – Released under MIT License This project was formerly known as Lotus (`lotus-validations`).