Vex
An extensible data validation library for Elixir.
Can be used to check different data types for compliance with criteria.
Ships with built-in validators to check for attribute presence, absence, inclusion, exclusion, format, length, acceptance, and by a custom function. You can easily define new validators and override existing ones.
Inspired by
- Rails ActiveModel Validations
- Clojure's Validateur
Supported Validations
Note the examples below use Vex.valid?/2
, with the validations to
check explicitly provided as the second argument. For information on how
validation configuration can be provided as part of a single argument
(eg, packaged with the data to check passed to Vex.valid?/1
) see
"Configuring Validations" below.
Note all validations can be skipped based on :if
and :unless
conditions given as options. See "Validation Conditions" further below for
more information.
Presence
Ensure a value is present:
Vex.valid? post, title: [presence: true]
See the documentation on Vex.Validators.Presence
for details on
available options.
Absence
Ensure a value is absent (blank)
Vex.valid? post, byline: [absence: true]
See the documentation on Vex.Validators.Absence
for details on
available options.
Inclusion
Ensure a value is in a list of values:
Vex.valid? post, category: [inclusion: ["politics", "food"]]
This validation can be skipped for nil
or blank values by including
allow_nil: true
and/or allow_blank: true
.
See the documentation on Vex.Validators.Inclusion
for details on available options.
Exclusion
Ensure a value is not in a list of values:
Vex.valid? post, category: [exclusion: ["oped", "lifestyle"]]
See the documentation on Vex.Validators.Exclusion
for details on available
options.
Format
Ensure a value matches a regular expression:
Vex.valid? widget, identifier: [format: ~r/(^id-)/]
This validation can be skipped for nil
or blank values by including
allow_nil: true
and/or allow_blank: true
.
See the documentation on Vex.Validators.Format
for details on
available options.
Length
Ensure a value's length is at least a given size:
Vex.valid? user, username: [length: [min: 2]]
Ensure a value's length is at or below a given size:
Vex.valid? user, username: [length: [max: 10]]
Ensure a value's length is within a range (inclusive):
Vex.valid? user, username: [length: [in: 2..10]]
This validation can be skipped for nil
or blank values by including
allow_nil: true
and/or allow_blank: true
.
See the documentation on Vex.Validators.Length
for details on
available options.
Number
Ensure a value is a number greater than a given number:
Vex.valid? value, number: [greater_than: 2]
Ensure a value is a number less than or equal to a given number:
Vex.valid? value, number: [less_than_or_equal_to: 10]
Ensure a value is a number is within a given range:
Vex.valid? value, number: [greater_than_or_equal_to: 0, less_than: 10]
This validation can be skipped for nil
or blank values by including
allow_nil: true
or allow_blank: true
respectively in the options.
See the documentation on Vex.Validators.Number
for details
on available options.
UUID
Ensure a value is a valid UUID string:
Vex.valid? value, uuid: true
Ensure a value is a valid UUID string in a given format:
Vex.valid? value, uuid: [format: :hex]
This validation can be skipped for nil
or blank values by including
allow_nil: true
or allow_blank: true
respectively in the options.
See the documentation on Vex.Validators.Uuid
for details
on available options.
Acceptance
Ensure an attribute is set to a positive (or custom) value. For use expecially with "acceptance of terms" checkboxes in web applications.
Vex.valid?(user, accepts_terms: [acceptance: true])
To check for a specific value, use :as
:
Vex.valid?(user, accepts_terms: [acceptance: [as: "yes"]])
See the documentation on Vex.Validators.Acceptance
for details on
available options.
Confirmation
Ensure a value has a matching confirmation:
Vex.valid? user, password: [confirmation: true]
The above would ensure the values of password
and
password_confirmation
are equivalent.
This validation can be skipped for nil
or blank values by
including allow_nil: true
and/or allow_blank: true
.
See the documentation on Vex.Validators.confirmation
for details
on available options.
Custom Function
You can also just provide a custom function for validation instead of a validator name:
Vex.valid?(user, password: fn (pass) -> byte_size(pass) > 4 end)
Vex.valid? user, password: &valid_password?/1
Vex.valid?(user, password: &(&1 != "god"))
Instead of returning a boolean the validate function may return :ok
on success, or {:error, "a message"}
on error:
Vex.valid?(user, password: fn (password) ->
if valid_password?(password) do
:ok
else
{:error, "#{password} isn't a valid password"}
end
end)
Or explicitly using :by
:
Vex.valid?(user, age: [by: &(&1 > 18)])
This validation can be skipped for nil
or blank values by including
allow_nil: true
and/or allow_blank: true
.
See the documentation on Vex.Validators.By
for details on available options.
Validation Conditions
A validation can be made applicable (or unapplicable) by using the :if
,
:if_any
, :unless
and :unless_any
options.
Note Vex.results
will return tuples with :not_applicable
for validations that
are skipped as a result of failing conditions.
Based on another attribute's presence
Require a post to have a body
of at least 200 bytes unless a non-blank
reference_url
is provided.
iex> Vex.valid?(post, body: [length: [min: 200, unless: :reference_url]])
Based on other attributes' presence
Require a post to have a body
of at least 200 bytes unless a non-blank
reference_url
and category
are provided.
iex> Vex.valid?(post, body: [length: [min: 200, unless: [:reference_url, :category]]])
Require a post to have a body
of at least 200 bytes unless a non-blank
reference_url
or category
is provided.
iex> Vex.valid?(post, body: [length: [min: 200, unless_any: [:reference_url, :category]]])
Based on another attribute's value
Only require a password if the state
of a user is :new
:
iex> Vex.valid?(user, password: [presence: [if: [state: :new]]]
Based on other attributes' value
Only require a password if the state
of a user is :new
and she is not from Facebook:
iex> Vex.valid?(user, password: [presence: [if: [state: :new, from_facebook: false]]]
Only require a password if the state
of a user is :new
or she is not from Facebook:
iex> Vex.valid?(user, password: [presence: [if_any: [state: :new, from_facebook: false]]]
Based on a custom function
Don't require users from Facebook to provide an email address:
iex> Vex.valid?(user, email: [presence: [unless: &User.from_facebook?/1]]
Require users less than 13 years of age to provide a parent's email address:
iex> Vex.valid?(user, parent_email: [presence: [if: &(&1.age < 13)]]
Configuring Validations
The examples above use Vex.valid?/2
, passing both the data to
be validated and the validation settings. This is nice for ad hoc data
validation, but wouldn't it be nice to just:
Vex.valid?(data)
... and have the data tell Vex which validations should be evaluated?
In Structs
In your struct module, use Vex.Struct
:
defmodule User do
defstruct username: nil, password: nil, password_confirmation: nil
use Vex.Struct
validates :username, presence: true,
length: [min: 4],
format: ~r/^[[:alpha:]][[:alnum:]]+$/
validates :password, length: [min: 4],
confirmation: true
end
Note validates
should only be used once per attribute.
Once configured, you can use Vex.valid?/1
:
user = %User{username: "actualuser",
password: "abcdefghi",
password_confirmation: "abcdefghi"}
Vex.valid?(user)
You can also use valid?
directly from the Module:
user |> User.valid?
In Keyword Lists
In your list, just include a :_vex
entry and use Vex.valid?/1
:
user = [username: "actualuser",
password: "abcdefghi",
password_confirmation: "abcdefghi",
_vex: [username: [presence: true,
length: [min: 4],
format: ~r/^[[:alpha:]][[:alnum:]]+$/]],
password: [length: [min: 4], confirmation: true]]
Vex.valid?(user)
Others
Just implement the Vex.Extract
protocol. Here's what was done to
support keyword lists:
defimpl Vex.Extract, for: List do
def settings(data) do
Keyword.get data, :_vex
end
def attribute(data, name) do
Keyword.get data, name
end
end
Querying Results
For validity, it's the old standard, Vex.valid?/1
:
iex> Vex.valid?(user)
true
(If you need to pass in the validations to use, do that as a second argument to
Vex.valid?/2
)
You can access the raw validation results using Vex.results/1
:
iex> Vex.results(user)
[{:ok, :username, :presence},
{:ok, :username, :length},
{:ok, :username, :format}]
If you only want the errors, use Vex.errors/1
:
iex> Vex.errors(another_user)
[{:error, :password, :length, "must have a length of at least 4"},
{:error, :password, :confirmation, "must match its confirmation"}]
Error Message Renderers
By default Vex uses Vex.ErrorRenderers.EEx
as default renderer, also have
Vex.ErrorRenderers.Parameterized
, and you have ability to define your own.
For example if we want to use Linguist for internationalization, we can do the following:
defmodule I18nErrorRenderer do
@behaviour Vex.ErrorRenderer
use Linguist.Vocabulary
locale "en", [
foo: [
too_short: "too short, min %{min} chars",
must_start_with_f: "must start with an f",
],
]
locale "kr", [
foo: [
too_short: "λ무 짧μΌλ©΄, μ΅μ %{min} κ° λ¬Έμ",
must_start_with_f: "fλ‘ μμν΄μΌν©λλ€",
],
]
def message(options, _default, context \\ []) do
message = options[:message] || raise "message is needed for proper i18n"
locale = options[:locale] || "en"
t!(locale, message, context)
end
end
result = Vex.validate([name: "Foo"], name: [
length: [
min: 4,
error_renderer: I18nErrorRenderer,
message: "foo.too_short"
],
format: [
with: ~r/^f/,
locale: "kr",
error_renderer: I18nErrorRenderer,
message: "foo.must_start_with_f",
]
])
assert {:error, [
{:error, :name, :length, "too short, min 4 chars"},
{:error, :name, :format, "fλ‘ μμν΄μΌν©λλ€"}
]} = result
We can set error renderer globally:
config :vex,
error_renderer: Vex.ErrorRenderers.Parameterized
Validators declare a list of the available message fields and their
descriptions by setting the module attribute @message_fields
(see
Vex.Validator.ErrorMessage
), and the metadata is available for querying:
iex> Vex.Validators.Length.__validator__(:message_fields)
[value: "Bad value", tokens: "Tokens from value", size: "Number of tokens",
min: "Minimum acceptable value", max: "Maximum acceptable value"]
Custom EEx Error Renderer Messages
Custom error messages can be requested by validations when providing the
:message
option and can use EEx to insert fields specific to the validator, eg:
validates :body, length: [min: 4,
tokenizer: &String.split/1,
message: "<%= length tokens %> words isn't enough"]
This could yield, in the case of a :body
value "hello my darling"
, the result:
{:error, "3 words isn't enough"}
Adding and Overriding Validators
Validators are simply modules that implement validate/2
and return :ok
or a tuple with :error
and a message. They usually use Vex.Validator
as well to get some common utilities for supporting :allow_nil
, :allow_blank
, and custom :message
options:
defmodule App.CurrencyValidator do
use Vex.Validator
def validate(value, options) do
# Return :ok or {:error, "a message"}
end
end
If you wanted to make this validator available to Vex as the :currency
validator so that you could do this:
validates :amount, currency: true
You just need to add a validator source so that Vex knows where to find it.
A source can be anything that implements the Vex.Validator.Source
protocol.
We'll use a keyword list for this example. The implementation for List
allows us to provide a simple mapping.
Vex uses Application.get_env(:vex, :sources)
to retrieve the
configuration of sources, defaulting to [Vex.Validators]
. We can
set the configuration with
Mix.Config,
as in:
config :vex,
sources: [[currency: App.CurrencyValidator], Vex.Validators]
Vex will consult the list of sources -- in order -- when looking for a
validator. By putting our new source before Vex.Validators
, we make it
possible to override the built-in validators.
Note: Without a sources
configuration, Vex falls back to a default of [Vex.Validators]
.
Using Modules as Sources
If adding mappings to our keyword list source becomes
tiresome, we can make use of the fact there's a Vex.Validator.Source
implementation for Atom
; we can provide a module name as a source instead
(just as Vex does with Vex.Validators
).
config :vex,
sources: [App.Validators, Vex.Validators]
If given an atom, Vex will assume it refers to a module and try two strategies to retrieve a validator:
- If the module exports a
validator/1
function, it will call that function, passing the validator name (eg,:currency
) - Otherwise, Vex will assume the validator module is the same as the
source module plus a dot and the camelized validator name (eg, given
a source of
App.Validators
, it would look for a:currency
validator atApp.Validators.Currency
)
In either case it will check the candidate validator for an exported
validate/2
function.
In the event no validators can be found for a name, a
Vex.InvalidValidatorError
will be raised.
Checking Validator Lookup
To see what validator Vex finds for a given validator name, use Vex.validator/1
:
iex> Vex.validator(:currency)
App.Validators.Currency
Contributing
Please fork and send pull requests (preferably from non-master branches), including tests (doctests or normal ExUnit.Case
tests).
Report bugs and request features via Issues; kudos if you do it from pull requests you submit that fix the bugs or add the features. ;)
License
Released under the MIT License.