Skip to content

pimbrouwers/Validus

main
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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
| Success p -> printfn "%A" p
| Failure 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.

About

An extensible F# validation library.

Resources

License

Stars

Watchers

Forks