Skip to content

pimbrouwers/Validus

Repository files navigation

Validus

NuGet Version build

Validus is an extensible validation library for F# with built-in validators for most primitive types and easily extended through custom validators.

Key Features

Quick Start

A common example of receiving input from an untrusted source PersonDto (i.e., HTML form submission), applying validation and producing a result based on success/failure.

open System
open System.Net.Mail
open Validus

type PersonDto =
    { FirstName : string
      LastName  : string
      Email     : string
      Age       : int option
      StartDate : DateTime option }

type Name =
    { First : string
      Last  : string }

type Person =
    { Name      : Name
      Email     : string
      Age       : int option
      StartDate : DateTime }

module Person =
    let ofDto (dto : PersonDto) =
        // A basic validator
        let nameValidator =
            Check.String.betweenLen 3 64

        // A custom email validator, using the *built-in* functionality
        // from System.Net.Mail
        let emailValidator =
            let msg = sprintf "Please provide a valid %s"
            let rule v =
                let success, _ = MailAddress.TryCreate v
                success
            Validator.create msg rule

        // Composing multiple validators to form complex validation rules,
        // overriding default error message (Note: "Check.WithMessage.String" as
        // opposed to "Check.String")
        let emailValidator =
            let emailPatternValidator =
                let msg = sprintf "Please provide a valid %s"
                Check.WithMessage.String.pattern @"[^@]+@[^\.]+\..+" msg

            ValidatorGroup(Check.String.betweenLen 8 512)
                .And(emailPatternValidator)
                .Build()

        // Defining a validator for an option value
        let ageValidator =
            Check.optional (Check.Int.between 1 100)

        // Defining a validator for an option value that is required
        let dateValidator =
            Check.required (Check.DateTime.greaterThan DateTime.Now)

        validate {
          let! first = nameValidator "First name" dto.FirstName
          and! last = nameValidator "Last name" dto.LastName
          and! email = emailValidator "Email address" dto.Email
          and! age = ageValidator "Age" dto.Age
          and! startDate = dateValidator "Start Date" dto.StartDate

          // Construct Person if all validators return Success
          return {
              Name = { First = first; Last = last }
              Email = email
              Age = age
              StartDate = startDate }
        }

Note: This is for demo purposes only, it likely isn't advisable to attempt to validate emails using a regular expression. Instead, use System.Net.MailAddress.

And, using the validator:

let dto : PersonDto =
    { FirstName = "John"
      LastName  = "Doe"
      Email     = "john.doe@url.com"
      Age       = Some 63
      StartDate = Some (new DateTime(2058, 1, 1)) }

match validatePersonDto dto with
| Ok p -> printfn "%A" p
| Error e ->
    e
    |> ValidationErrors.toList
    |> Seq.iter (printfn "%s")

Validating Complex Types

Included in Validus is an applicative computation expression, which in this case allow validation errors to be accumulated as validators are executed.

open Validus

type PersonDto =
    { FirstName : string
      LastName  : string
      Age       : int option }

type Name =
    { First : string
      Last  : string }

type Person =
    { Name      : Name
      Age       : int option }

module Person =
    let ofDto (dto : PersonDto) =
        let nameValidator = Check.String.betweenLen 3 64

        let firstNameValidator =
            ValidatorGroup(nameValidator)
                .Then(Check.String.notEquals dto.LastName)
                .Build()

        validate {
          let! first = firstNameValidator "First name" dto.FirstName
          and! last = nameValidator "Last name" dto.LastName
          and! age = Check.optional (Check.Int.between 1 120) "Age" dto.Age

          return {
              Name = { First = first; Last = last }
              Age = age }
        }

Creating A Custom Validator

open System.Net.Mail
open Validus

let fooValidator =
    let fooRule v = v = "foo"
    let fooMessage = sprintf "%s must be a string that matches 'foo'"
    Validator.create fooMessage fooRule

"bar"
|> fooValidator "Test string"

Combining Validators

Complex validator chains and waterfalls can be created by combining validators together using the ValidatorGroup API. Alternatively, a full suite of operators are available, for those who prefer that style of syntax.

open System.Net.Mail
open Validus

let emailPatternValidator =
    let msg = sprintf "The %s input is not formatted as expected"
    Check.WithMessage.String.pattern @"[^@]+@[^\.]+\..+" msg

// A custom validator that uses System.Net.Mail to validate email
let mailAddressValidator =
    let msg = sprintf "The %s input is not a valid email address"
    let rule (x : string) =
        let success, _ = MailAddress.TryCreate x
        success
    Validator.create msg rule

let emailValidator =
    ValidatorGroup(Check.String.betweenLen 8 512)
        .And(emailPatternValidator)
        .Then(mailAddressValidator) // only executes when prior two steps are `Ok`
        .Build()

"fake@test"
|> emailValidator "Login email"

We can use any validator, or combination of validators to validate collections:

let emails = [ "fake@test"; "bob@fsharp.org"; "x" ]

let result =
    emails
    |> List.map (emailValidator "Login email")

Value Objects

It is generally a good idea to create value objects, sometimes referred to a value types or constrained primitives, to represent individual data points that are more classified than the primitive types usually used to represent them.

Example 1: Email Address Value Object

A good example of this is an email address being represented as a string literal, as it exists in many programs. This is however a flawed approach in that the domain of an email address is more tightly scoped than a string will allow. For example, "" or null are not valid emails.

To address this, we can create a wrapper type to represent the email address which hides away the implementation details and provides a smart construct to produce the type.

open System.Net.Mail

type Email =
    private { Email : string }

    override x.ToString () = x.Email

    // Note the transformation from string -> Email
    static member Of : Validator<string, Email> = fun field input ->
        let rule (x : string) =
            if x = "" then false
            else
                try
                    let addr = MailAddress(x)
                    if addr.Address = x then true
                    else false
                with
                | :? FormatException -> false

        let message = sprintf "%s must be a valid email address"

        input
        |> Validator.create message rule field
        |> Result.map (fun v -> { Email = v })

Example 2: E164 Formatted Phone Number

type E164 =
    private { E164 : string }

    override x.ToString() = x.E164

    static member Of : Validator<string, E164> = fun field input ->
        let e164Regex = @"^\+[1-9]\d{1,14}$"
        let message = sprintf "%s must be a valid E164 telephone number"

        input
        |> Check.WithMessage.String.pattern e164Regex message field
        |> Result.map (fun v -> { E164 = v })

Built-in Validators

Note: Validators pre-populated with English-language default error messages reside within the Check module.

equals

Applies to: string, int16, int, int64, decimal, float, DateTime, DateTimeOffset, TimeSpan, 'a array, 'a list, 'a seq

open Validus

// Define a validator which checks if a string equals
// "foo" displaying the standard error message.
let equalsFoo =
  Check.String.equals "foo" "fieldName"

equalsFoo "bar"

// Define a validator which checks if a string equals
// "foo" displaying a custom error message (string -> string).
let equalsFooCustom =
  let msg = sprintf "%s must equal the word 'foo'"
  Check.WithMessage.String.equals "foo" msg "fieldName"

equalsFooCustom "bar"

notEquals

Applies to: string, int16, int, int64, decimal, float, DateTime, DateTimeOffset, TimeSpan, 'a array, 'a list, 'a seq

open Validus

// Define a validator which checks if a string is not
// equal to "foo" displaying the standard error message.
let notEqualsFoo =
  Check.String.notEquals "foo" "fieldName"

notEqualsFoo "bar"

// Define a validator which checks if a string is not
// equal to "foo" displaying a custom error message (string -> string)
let notEqualsFooCustom =
  let msg = sprintf "%s must not equal the word 'foo'"
  Check.WithMessage.String.notEquals "foo" msg "fieldName"

notEqualsFooCustom "bar"

between

Applies to: int16, int, int64, decimal, float, DateTime, DateTimeOffset, TimeSpan

open Validus

// Define a validator which checks if an int is between
// 1 and 100 (inclusive) displaying the standard error message.
let between1and100 =
  Check.Int.between 1 100 "fieldName"

between1and100 12 // Result<int, ValidationErrors>

// Define a validator which checks if an int is between
// 1 and 100 (inclusive) displaying a custom error message.
let between1and100Custom =
  let msg = sprintf "%s must be between 1 and 100"
  Check.WithMessage.Int.between 1 100 msg "fieldName"

between1and100Custom 12 // Result<int, ValidationErrors>

greaterThan

Applies to: int16, int, int64, decimal, float, DateTime, DateTimeOffset, TimeSpan

open Validus

// Define a validator which checks if an int is greater than
// 100 displaying the standard error message.
let greaterThan100 =
  Check.Int.greaterThan 100 "fieldName"

greaterThan100 12 // Result<int, ValidationErrors>

// Define a validator which checks if an int is greater than
// 100 displaying a custom error message.
let greaterThan100Custom =
  let msg = sprintf "%s must be greater than 100"
  Check.WithMessage.Int.greaterThan 100 msg "fieldName"

greaterThan100Custom 12 // Result<int, ValidationErrors>

greaterThanOrEqualTo

Applies to: int16, int, int64, decimal, float, DateTime, DateTimeOffset, TimeSpan

open Validus

// Define a validator which checks if an int is greater than
// or equal to 100 displaying the standard error message.
let greaterThanOrEqualTo100 =
  Check.Int.greaterThanOrEqualTo 100 "fieldName"

greaterThanOrEqualTo100 12 // Result<int, ValidationErrors>

// Define a validator which checks if an int is greater than
// or equal to 100 displaying a custom error message.
let greaterThanOrEqualTo100Custom =
  let msg = sprintf "%s must be greater than or equal to 100"
  Check.WithMessage.Int.greaterThanOrEqualTo 100 msg "fieldName"

greaterThanOrEqualTo100Custom 12 // Result<int, ValidationErrors>

lessThan

Applies to: int16, int, int64, decimal, float, DateTime, DateTimeOffset, TimeSpan

open Validus

// Define a validator which checks if an int is less than
// 100 displaying the standard error message.
let lessThan100 =
  Check.Int.lessThan 100 "fieldName"

lessThan100 12 // Result<int, ValidationErrors>

// Define a validator which checks if an int is less than
// 100 displaying a custom error message.
let lessThan100Custom =
  let msg = sprintf "%s must be less than 100"
  Check.WithMessage.Int.lessThan 100 msg "fieldName"

lessThan100Custom 12 // Result<int, ValidationErrors>

lessThanOrEqualTo

Applies to: int16, int, int64, decimal, float, DateTime, DateTimeOffset, TimeSpan

open Validus

// Define a validator which checks if an int is less than
// or equal to 100 displaying the standard error message.
let lessThanOrEqualTo100 =
  Check.Int.lessThanOrEqualTo 100 "fieldName"

lessThanOrEqualTo100 12 // Result<int, ValidationErrors>

// Define a validator which checks if an int is less than
// or equal to 100 displaying a custom error message.
let lessThanOrEqualTo100Custom =
  let msg = sprintf "%s must be less than or equal to 100"
  Check.WithMessage.Int.lessThanOrEqualTo 100 msg "fieldName"

lessThanOrEqualTo100Custom 12 // Result<int, ValidationErrors>

betweenLen

Applies to: string, 'a array, 'a list, 'a seq

open Validus

// Define a validator which checks if a string is between
// 1 and 100 chars displaying the standard error message.
let between1and100Chars =
  Check.String.betweenLen 1 100 "fieldName"

between1and100Chars "validus"

// Define a validator which checks if a string is between
// 1 and 100 chars displaying a custom error message.
let between1and100CharsCustom =
  let msg = sprintf "%s must be between 1 and 100 chars"
  Check.WithMessage.String.betweenLen 1 100 msg "fieldName"

between1and100CharsCustom "validus"

equalsLen

Applies to: string, 'a array, 'a list, 'a seq

open Validus

// Define a validator which checks if a string is equals to
// 100 chars displaying the standard error message.
let equals100Chars =
  Check.String.equalsLen 100 "fieldName"

equals100Chars "validus"

// Define a validator which checks if a string is equals to
// 100 chars displaying a custom error message.
let equals100CharsCustom =
  let msg = sprintf "%s must be 100 chars"
  Check.WithMessage.String.equalsLen 100 msg "fieldName"

equals100CharsCustom "validus"

greaterThanLen

Applies to: string, 'a array, 'a list, 'a seq

open Validus

// Define a validator which checks if a string is greater than
// 100 chars displaying the standard error message.
let greaterThan100Chars =
  Check.String.greaterThanLen 100 "fieldName"

greaterThan100Chars "validus"

// Define a validator which checks if a string is greater than
// 100 chars displaying a custom error message.
let greaterThan100CharsCustom =
  let msg = sprintf "%s must be greater than 100 chars"
  Check.WithMessage.String.greaterThanLen 100 msg "fieldName"

greaterThan100CharsCustom "validus"

greaterThanOrEqualToLen

Applies to: string, 'a array, 'a list, 'a seq

open Validus

// Define a validator which checks if a string is greater than
// or equal to 100 chars displaying the standard error message.
let greaterThanOrEqualTo100Chars =
  Check.String.greaterThanOrEqualToLen 100 "fieldName"

greaterThanOrEqualTo100Chars "validus"

// Define a validator which checks if a string is greater than
// or equal to 100 chars displaying a custom error message.
let greaterThanOrEqualTo100CharsCustom =
  let msg = sprintf "%s must be greater than or equal to 100 chars"
  Check.WithMessage.String.greaterThanOrEqualToLen 100 msg "fieldName"

greaterThanOrEqualTo100CharsCustom "validus"

lessThanLen

Applies to: string, 'a array, 'a list, 'a seq

open Validus

// Define a validator which checks if a string is less tha
// 100 chars displaying the standard error message.
let lessThan100Chars =
  Check.String.lessThanLen 100 "fieldName"

lessThan100Chars "validus"

// Define a validator which checks if a string is less tha
// 100 chars displaying a custom error message.
let lessThan100CharsCustom =
  let msg = sprintf "%s must be less than 100 chars"
  Check.WithMessage.String.lessThanLen 100 msg "fieldName"

lessThan100CharsCustom "validus"

lessThanOrEqualToLen

Applies to: string, 'a array, 'a list, 'a seq

open Validus

// Define a validator which checks if a string is less tha
// or equal to 100 chars displaying the standard error message.
let lessThanOrEqualTo100Chars =
  Check.String.lessThanOrEqualToLen 100 "fieldName"

lessThanOrEqualTo100Chars "validus"

// Define a validator which checks if a string is less tha
// or equal to 100 chars displaying a custom error message.
let lessThanOrEqualTo100CharsCustom =
  let msg = sprintf "%s must be less than 100 chars"
  Check.WithMessage.String.lessThanOrEqualToLen 100 msg "fieldName"

lessThanOrEqualTo100CharsCustom "validus"

empty

Applies to: string, 'a array, 'a list, 'a seq

open Validus

// Define a validator which checks if a string is empty
// displaying the standard error message.
let stringIsEmpty =
  Check.String.empty "fieldName"

stringIsEmpty "validus"

// Define a validator which checks if a string is empty
// displaying a custom error message.
let stringIsEmptyCustom =
  let msg = sprintf "%s must be empty"
  Check.WithMessage.String.empty msg "fieldName"

stringIsEmptyCustom "validus"

notEmpty

Applies to: string, 'a array, 'a list, 'a seq

open Validus

// Define a validator which checks if a string is not empty
// displaying the standard error message.
let stringIsNotEmpty =
  Check.String.notEmpty "fieldName"

stringIsNotEmpty "validus"

// Define a validator which checks if a string is not empty
// displaying a custom error message.
let stringIsNotEmptyCustom =
  let msg = sprintf "%s must not be empty"
  Check.WithMessage.String.notEmpty msg "fieldName"

stringIsNotEmptyCustom "validus"

pattern

Applies to: string

open Validus

// Define a validator which checks if a string matches the
// provided regex displaying the standard error message.
let stringIsChars =
  Check.String.pattern "[a-z]+" "fieldName"

stringIsChars "validus"

// Define a validator which checks if a string matches the
// provided regex displaying a custom error message.
let stringIsCharsCustom =
  let msg = sprintf "%s must follow the pattern [a-z]"
  Check.WithMessage.String.pattern "[a-z]" msg "fieldName"

stringIsCharsCustom "validus"

exists

Applies to: 'a array, 'a list, 'a seq

open Validus

// Define a validator which checks if a collection matches the provided predicate
// displaying the standard error message.
let collectionContains =
  Check.List.exists (fun x -> x = 1) "fieldName"

collectionContains [1]

// Define a validator which checks if a string is not empty
// displaying a custom error message.
let collectionContainsCustom =
  let msg = sprintf "%s must contain the value '1'"
  Check.WithMessage.List.exists (fun x -> x = 1) msg "fieldName"

collectionContainsCustom [1]

Custom Operators

Operator Description
<+> Compose two validators of equal types
*|* Map the Ok result of a validator, high precedence, for use with choice <|>.
*| Set the Ok result of a validator to a fixed value, high precedence, for use with choice <|>.
>>| Map the Ok result of a validator, low precedence, for use in chained validation
>| Set the Ok result of a validator to a fixed value, low precedence, for use in chained validation
>>= Bind the Ok result of a validator with a one-argument function that returns a Result
<<= Reverse-bind the Ok result of a validator with a one-argument function that returns a Result
>>% Set the Ok result of a validator to a fixed Result value
<|> Introduce choice: if the rh-side validates Ok, pick that result, otherwise, continue with the next validator
>=> Kleisli-bind two validators. Other than Compose <+>, this can change the result type.
<=< Reverse kleisli-bind two validators (rh-side is evaluated first). Other than Compose <+>, this can change the result type.
.>> Compose two validators, but keep the result of the lh-side. Ignore the result of the rh-side, unless it returns an Error.
>>. Compose two validators, but keep the result of the rh-side. Ignore the result of the lh-side, unless it returns an Error.
.>>. Compose two validators, and keep the result of both sides as a tuple.

Recreating the example code above using the combinator operators:

open System.Net.Mail
open Validus
open Validus.Operators

let msg = sprintf "Please provide a valid %s"

let emailPatternValidator =
    Check.WithMessage.String.pattern @"[^@]+@[^\.]+\..+" msg

// A custom validator that uses System.Net.Mail to validate email
let mailAddressValidator =
    let rule (x : string) =
        if x = "" then false
        else
            try
                let addr = MailAddress(x)
                if addr.Address = x then true
                else false
            with
            | :? FormatException -> false

    Validator.create msg rule

let emailValidator =
    Check.String.betweenLen 8 512 // check string is between 8 and 512 chars
    <+> emailPatternValidator     // and, check string match email regex
    >=> mailAddressValidator      // then, check using System.Net.Mail if prior two steps are `Ok`

"fake@test"
|> emailValidator "Login email"

A more complex example involving "chained" validators and both "choice" assignment & mapping:

open System
open Validus
open Validus.Operators

type AgeGroup =
    | Adult of int
    | Child
    | Senior

let ageValidator =
    Check.String.pattern @"\d+" *|* Int32.Parse // if pattern matches, convert to Int32
    >=> Check.Int.between 0 120                 // first check age between 0 and 120
    >=> (Check.Int.between 0 17  *| Child       // then, check age between 0 an 17 assigning Child
    <|> Check.Int.greaterThan 65 *| Senior      // or, check age greater than 65 assiging Senior
    <|> Check.Int.between 18 65  *|* Adult)     // or, check age between 18 and 65 assigning adult mapping converted input

Find a bug?

There's an issue for that.

License

Built with ♥ by Pim Brouwers in Toronto, ON. Licensed under Apache License 2.0.