Module Validator

Create a record validator via composable sub-validators. The input and the output can be different types.

type input_form = {
  name: string;
  email: string option;
  age: int;
}

type valid_form = {
  name: string;
  email: string;
  age: int;
}

let build_valid_form name email age =
  { name; email; age }

let validator_name =
  let open Validator in
  string_is_not_empty "Empty"
  |> compose
    (string_has_min_length 3 "Name is too short")

let validator_email =
  let open Validator in
  option_is_some "Missing email"
  |> compose (string_is_email "Not an email")

let validator_age =
  let open Validator in
  int_min 13 "Must be 13"

let validator (input: input_form) =
  let open Validator in
  build build_valid_form
  |> validate input.name validator_name
  |> validate input.email validator_email
  |> validate input.age validator_age

validator { name = "Sam"; email = Some "sam@sample.com"; age = 20}

==>

Ok { name = "Sam"; email = "sam@sample.com"; age = 20}
type 'err errors = 'err * 'err list

Validator errors are a tuple of (error, error list)

The first element in the tuple is the first error. The second element is a list of all errors (including the first one)

type ('out, 'err) validator_result = ('out'err errors) Stdlib.result

A validator returns a result of:

  • Ok output or
  • Error (err, err list)
type ('input, 'output, 'err) validator = 'input -> ('output'err) validator_result
type ('input, 'output, 'err) validator_builder = 'err -> ('input'output'err) validator
val int_min : int -> (int, int, 'err) validator_builder

Validate min permitted integeger

let validator input =
  let open Validator in
  build build_valid
  |> validate input.age (int_min 13 "Must be 13")
val int_max : int -> (int, int, 'err) validator_builder

Validate max permitted integeger

let validator input =
  let open Validator in
  build build_valid
  |> validate input.age (int_max 6 "Must be 6 or younger")
val list_is_not_empty : ('a list'a list'err) validator_builder

Validate that a list is not empty

let validator input =
  let open Validator in
  build build_valid
  |> validate input.hobbies (list_is_not_empty "Must have a hobby")
val list_has_max_length : int -> ('a list'a list'err) validator_builder

Validate max length of a list

let validator input =
  let open Validator in
  build build_valid
  |> validate input.hobbies (list_has_max_length 12 "Max 12 hobbies")
val list_has_min_length : int -> ('a list'a list'err) validator_builder

Validate min length of a list

let validator input =
  let open Validator in
  build build_valid
  |> validate input.hobbies (list_has_min_length 2 "Min 2 hobbies")
val list_every : ('i'o'err) validator -> ('i list'o list'err) validator

Validate a list of items. Run the given validator for each item returning all the errors.

let hobbie_validator =
  let open Validator in
  string_is_not_empty "Must not be empty"

let validator input =
  let open Validator in
  build build_valid
  |> validate input.hobbies (list_every hobbie_validator)
val option_is_some : ('a option'a'err) validator_builder

Validate that a value is not None. Returns the value if Some.

let validator input =
  let open Validator in
  build build_valid
  |> validate input.name (option_is_some "Must be present")
val string_is_not_empty : (string, string, 'err) validator_builder

Validate if a string is not empty

let validator input =
  let open Validator in
  build build_valid
  |> validate input.name (string_is_not_empty "Must not be blank")
val string_is_int : (string, int, 'err) validator_builder

Validate if a string parses to an Int. Returns the Int if so

let validator input =
  let open Validator in
  build build_valid
  |> validate input.age (string_is_int "Must be a number")
val string_has_min_length : int -> (string, string, 'err) validator_builder

Validate the min length of a string

let validator input =
  let open Validator in
  build build_valid
  |> validate input.password (string_has_min_length 3 "Min 3 chars")
val string_has_max_length : int -> (string, string, 'err) validator_builder

Validate the max length of a string

let validator input =
  let open Validator in
  build build_valid
  |> validate input.password (string_has_max_length 100 "Max 100 chars")
val string_is_email : (string, string, 'err) validator_builder

Validate if a string is an email. This checks if a string follows a simple pattern `_@_`.

let validator input =
  let open Validator in
  build build_valid
  |> validate input.email (string_is_email "Not an email")
val optional : ('i'o'err) validator -> 'i option -> ('i option'err errors) Stdlib.result

Validate an optional value. Run the validator only if the value is Some. If the value is None then just return None back.

let message_validator =
  Validator.string_is_not_empty "Must not be blank"

let validator input =
  let open Validator in
  build build_valid
  |> validate input.message (optional message_validator)
val keep : 'a -> ('a -> 'next_acc'e errors) Stdlib.result -> ('next_acc'e errors) Stdlib.result

Keep a value as is.

let validator input =
  let open Validator in
  build build_valid
  |> keep input.message
val build : ('a -> 'final) -> ('a -> 'final'e errors) Stdlib.result

Start the build pipeline for a validator

let validator input =
  let open Validator in
  build build_valid
  |> validate input.age (int_min 13 "Must be 13")
  |> validate input.email (string_is_email "Must be email")
  |> keep input.message
val validate : 'input -> ('input'output'err) validator -> ('output -> 'next_acc'err errors) Stdlib.result -> ('next_acc'err errors) Stdlib.result

Chain validators

let validator input =
  let open Validator in
  build build_valid
  |> validate input.age (int_min 13 "Must be 13")
  |> validate input.email (string_is_email "Must be email")
  |> keep input.message
val compose : ('mid'o'err) validator -> ('i'mid'err) validator -> ('i'o'err) validator

Compose validators Run the first validator and if successful then the second. Only returns the first error.

let name_validator =
  open Validator in
  string_is_not_empty "Empty"
  |> compose (string_has_min_length 3 "Too short")

let validator input =
  let open Validator in
  build build_valid
  |> validate input.name name_validator
val all : ('io'io'err) validator list -> ('io'io'err) validator

Validate a value using a list of validators. This runs all the validators in the list.

The initial input is passed to all validators. All these validators must have the same input and output types.

Returns Ok when all validators pass. Returns Error when any validator fails. Error will have all failures.

let validators =
  let open Validator in
  [
    string_is_not_empty "Empty";
    string_has_min_length 4 "Min";
    string_has_max_length 20 "Max";
    string_is_email "Email";
  ]

let validator input =
  let open Validator in
  build build_valid
  |> validate input.email (all validators)
val whole : ('whole -> ('whole'err) Stdlib.result) -> ('whole'err) validator_result -> ('whole'err) validator_result

Validate a structure as a whole.

Sometimes we need to validate a property in relation to another.

This is just a function that takes the output and return a result.

let validate_whole person =
  if person.name == "Sam" then
    Ok person
  else
    Error "Not Sam"

let validator input =
  let open Validator in
  build build_valid
  |> validate input.email (...)
  |> whole validate_whole