valor: Simple general structured validation library

This is a package candidate release! Here you can preview how this package release will appear once published to the main package index (which can be accomplished via the 'maintain' link below). Please note that once a package has been published to the main package index it cannot be undone! Please consult the package uploading documentation for more information.

[maintain] [Publish]

Please see the README on GitHub at https://github.com/reygoch/valor#readme


[Skip to Readme]

Properties

Versions 0.0.0.1, 0.1.0.0, 0.1.0.0, 1.0.0.0
Change log ChangeLog.md
Dependencies base (>=4.7 && <5), transformers [details]
License MIT
Copyright 2018 Luka Hadžiegrić
Author Luka Hadžiegrić
Maintainer reygoch@gmail.com
Category Validation
Home page https://github.com/reygoch/valor#readme
Bug tracker https://github.com/reygoch/valor/issues
Source repo head: git clone https://github.com/reygoch/valor
Uploaded by LukaHadziegric at 2018-06-29T20:56:31Z

Modules

[Index]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees


Readme for valor-0.1.0.0

[back to package description]

Valor Build Status Valor Documentation

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:


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.

Lets declare our imports and required language extensions:

{-# LANGUAGE FlexibleInstances    #-}
{-# LANGUAGE StandaloneDeriving   #-}
{-# LANGUAGE TypeSynonymInstances #-}
--
module Tutorial where
--
import Prelude hiding ( id ) -- just so that we can use id as a field name
                             -- without any difficulties
import Data.Valor

import Data.Functor.Identity ( Identity (..) )
import Control.Monad.Trans.Except ( ExceptT, runExceptT, throwE )

import Data.Text ( Text )
import qualified Data.Text as Text
--

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):

data User = User
  { username :: Text
  , password :: Text
  } deriving ( Show )

data UserError = UserError
  { username :: Maybe String   -- this field will have only one error
  , password :: Maybe [String] -- this one can have multiple errors
  } deriving ( Show )

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:

data User' a = User
  { email    :: Validatable a String   Text
  , username :: Validatable a [String] Text
  }

type User = User' Identity
deriving instance Show User

type UserError = User' Validate
deriving instance Show UserError

This is equivalent to the first example, but much more maintainable. With this approach we have to use StandaloneDeriving and TypeSynonymInstances language extensions to allow us instance derivation.

Right, let's define a more complex type now:

data Article' a = Article
  { id      :: Validatable a String            Int
  , title   :: Validatable a [String]          Text
  , content :: Validatable a [String]          Text
  , tags1   :: Validatable a [Maybe [String]]  [Text] -- Here I want to have
                                                      -- multiple errors for a
                                                      -- single tag in a list.

  , tags2   :: Validatable a [Maybe String]    [Text] -- Here I want to have
                                                      -- only one reported
                                                      -- error per tag. 
  , author  :: Validatable a UserError         User
  , authors :: Validatable a [Maybe UserError] [User]
  }

type Article = Article' Identity
deriving instance Show Article

type ArticleError = Article' Validate
deriving instance Show ArticleError

Validatable type family is nothing smart. In fact, it is just a simple type level function. Here is its definition, it should be obvious what it does:

type family Validatable a e x where
  Validatable Validate e x = Maybe e
  Validatable Identity e x = x
  Validatable a        e x = a x

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:

nonover18 :: Monad m => Int -> ExceptT String m Int
nonover18 n = if n < 18
  then throwE "must be greater than 18"
  else pure n

nonempty' :: Monad m => Text -> ExceptT String m Text
nonempty' t = if Text.null t
  then throwE "can't be empty"
  else pure t

nonempty :: Monad m => Text -> ExceptT [String] m Text
nonempty t = if Text.null t
  then throwE ["can't be empty"]
  else pure t

nonbollocks :: Monad m => Text -> ExceptT [String] m Text
nonbollocks t = if t == "bollocks"
  then throwE ["can't be bollocks"]
  else pure t

nonshort :: Monad m => Text -> ExceptT [String] m Text
nonshort t = if Text.length t < 10
  then throwE ["too short"]
  else pure t

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.

With that out of our way we can start writing our 'Validator's:

userValidator :: Monad m => Validator User m UserError
userValidator = User
  <$> check  email nonempty'
  <*> checks username [nonempty, nonbollocks, nonshort]

articleValidator :: Monad m => Validator Article m ArticleError
articleValidator = Article
  <$> check           id      nonover18
  <*> checks          title   [nonempty, nonbollocks]
  <*> checks          content [nonempty, nonshort, nonbollocks]
  <*> mapChecks       tags1   [nonempty, nonbollocks]
  <*> mapCheck        tags2   nonempty'
  <*> subValidator    author  userValidator
  <*> mapSubValidator authors userValidator

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) .

Validating data

Let's define some sample data to test our Validator on:

goodUser :: User
goodUser = User "hello@kitty.com" "kittyusername"

badUser :: User
badUser = User "boaty@mcboatface.com" "bollocks"

badArticle :: Article
badArticle = Article
  { id      = 17
  , title   = ""
  , content = "bollocks"
  , tags1   = ["", "tag01", "tag02"]
  , tags2   = ["tag01", ""]
  , author  = badUser
  , authors = [badUser, goodUser]
  }

And now we can run our Article Validator against some actual data and here is how it's done:

>>> validatePure articleValidator badArticle
Just
  ( Article
      { id = Just "must be greater than 18"
      , title = Just ["can't be empty"]
      , content = Just ["too short","can't be bollocks"]
      , tags1 = Just [Just ["can't be empty"],Nothing,Nothing]
      , tags2 = Just [Nothing,Just "can't be empty"]
      , author = Just
          ( User
              { email = Nothing
              , username = Just ["can't be bollocks","too short"]
              }
          )
      , authors = Just
        [ Just
            ( User
                { email = Nothing
                , username = Just ["can't be bollocks","too short"]
                }
            )
        ,Nothing
        ]
      }
  )

That's all folks! To learn more read the actual documentation.