Maintainer's Corner

Readme for valor-0.1.0.0

Valor

General, simple and easy to use structured validation library that gives you
control over the error and input types.

Motivation

Currently there are a few validation libraries out there, most notably
forma and digestive-functors. They are acceptable but they are mostly
geard towards the web. Even with that in mind, I find them a bit impractical to
use so I've decided to make a validation library of my own.

In no particular order here are the main problems I have with them:

They limit what you can validate and what you get as a result of that
validation. Forma expects a JSON Value as an input and gives you either parsed data as a result or an error in the form of a JSON Value.

Additionally, digestive-functors use a custom result type that you need
to get familiar with to some extent.

They are essentially parsers, and I personally don't like to manually handle
conversion of JSON fields from e.g. string to integers and other data types.

Sure, it might be useful to tell the user that he entered text instead of a
number, but in that case I'd argue that your submission form is bad.

Even in this case, it should still be possible to validate plain JSON with
Valor, but if that is your use case I'd recommend you use forma for that
since it was specifically designed with JSON in mind.

They don't really play well with servant. Let's say that we have a record
SomeData. If we wanted to allow users to submit that data to the server we'd
have something like this :

"api" :> ReqBody '[JSON] SomeData :> Post '[JSON] SomeResponse

User would send SomeData encoded as JSON to the server, servant would
automagically parse it and pass it to the Handler for further processing.

If we wanted to validate this data with let's say forma than we would
have to write something like this:

"api" :> ReqBody '[JSON] Value :> Post '[JSON] SomeResponse

in which case we lose nice semantics from the first example where it is
obvious what data is being sent to the server (or at least what should've been
sent).

Since servant doesn't allow us to declare validation in the type,
validation always has to happen in the Handler at which point it is no
longer in the JSON form and library like forma is not of much use to us
unless we convert SomeData to JSON and parse it once again.

Tutorial

Before we get started, Valor uses ExceptT from the transformers package so make sure you add it as a dependency in your project.

Defining data types

First thing we usually want to do is to define our input data and error types.
We can define them separately by hand, or if our error and data types have the
same "shape" (same field names) we can use a handy type family to help us do
them all at once.

With that out of the way we can start defining our data types. As previously
stated, we can make both data and error types by hand like this (which would
additionally require the use of DuplicateRecordField extension):

This approach is perfectly valid and much more flexible since it allows you to
have different fields in data and error type, but if you want your types to
have the same field names than it might be easier to use Validatable type
family to get rid of the boilerplate and define them like this:

Creating a Validator

Ok, so now we have seen how Validatable type family works, we have defined
data types that we want to validate and data types that will store our errors.
Before we start writing our validation rules (Validators) we first need to
have some tests / checks to run against our field values so let's define some
simple ones:

Here the ExceptT transformer is used because it allows you to use your own
monad in case you need to access the database to validate some data. This is
also handy in case your test depends on the success or failure of some other
field value. In that case you can use the State monad or transformer to pass
in the full data being validated instead of just a current field value.

As you can see, it is very simple and readable code. You just state what field
you want to validate and what tests you want to run against it. As a result you
get your error type (once you've ran your Validator against some actual data)
.