arowM / elm-form-validator / Validator

This module provides a scalable way to validate a form by combining primitive validators.

For example, let's assume a form having two inputs as follows.

type alias Form =
    { sampleInput : Maybe Int
    , anotherInput : Maybe String
    }

The first step is to define a validator for each input.

import Regex

type SampleError
    = SampleBoundError
    | SampleRequiredError

sampleValidator : Validator (Maybe Int) SampleError
sampleValidator =
    required SampleRequiredError <|
        concat
            [ minBound SampleBoundError 10
            , maxBound SampleBoundError 20
            ]

errors sampleValidator (Just 15)
--> []

errors sampleValidator (Just 30)
--> [ SampleBoundError ]

errors sampleValidator Nothing
--> [ SampleRequiredError ]

isValid sampleValidator (Just 15)
--> True

isValid sampleValidator (Just 30)
--> False


type AnotherError
    = AnotherLengthError
    | AnotherPatternError

anotherValidator : Validator (Maybe String) AnotherError
anotherValidator =
    optional <|
        concat
            [ maxLength AnotherLengthError 20
            , pattern AnotherPatternError <| Maybe.withDefault Regex.never <| Regex.fromString "^(http://|https://)"
            ]

errors anotherValidator Nothing
--> []

errors anotherValidator (Just "foo")
--> [ AnotherPatternError ]

errors anotherValidator (Just "https://foo")
--> []

errors anotherValidator (Just "https://tooooooooooolong")
--> [ AnotherLengthError ]

errors anotherValidator (Just "ftp://tooooooooooolong")
--> [ AnotherLengthError, AnotherPatternError ]

isValid anotherValidator Nothing
--> True

isValid anotherValidator (Just "foo")
--> False

The next step is combining these validators to create a validator for the entire form.

type FormError
    = SampleError SampleError
    | AnotherError AnotherError

formValidator : Validator Form FormError
formValidator =
    concat
        [ liftMap SampleError .sampleInput sampleValidator
        , liftMap AnotherError .anotherInput anotherValidator
        ]

errors formValidator
    { sampleInput = Just 15
    , anotherInput = Just "https://foo"
    }
--> []

errors formValidator
    { sampleInput = Nothing
    , anotherInput = Nothing
    }
--> [ SampleError SampleRequiredError ]

errors formValidator
    { sampleInput = Nothing
    , anotherInput = Just "foo"
    }
--> [ SampleError SampleRequiredError
--> , AnotherError AnotherPatternError
--> ]

displayFormError : FormError -> String
displayFormError err =
    case err of
        SampleError SampleRequiredError ->
            "Sample Input cannot be empty"
        SampleError SampleBoundError ->
            "Sample Input is out of bounds"
        AnotherError AnotherLengthError ->
            "Length of Another Input is toooo long"
        AnotherError AnotherPatternError ->
            "Another Input must begin with `http://` or `https://`"

List.map displayFormError <|
    errors formValidator
        { sampleInput = Nothing
        , anotherInput = Nothing
        }
--> [ "Sample Input cannot be empty" ]

Types


type Validator a err

An opaque type representing validator for value of type a.

Functions to run Validator

errors : Validator a err -> a -> List err

Run validator to a target value and returns all validation errors.

isValid : Validator a err -> a -> Basics.Bool

The isValid only checks if a target value is valid or not.

Primitive Validators

succeed : Validator a err

A constructor for Validator which always results to valid.

isValid succeed "foo"
--> True

isValid succeed <| Just 34
--> True

errors (required "Required error" succeed) <| Nothing
--> [ "Required error" ]

fail : err -> Validator a err

A constructor for Validator which always results to invalid.

errors (fail "error") "foo"
--> [ "error" ]

errors (fail "error") <| Just 34
--> [ "error" ]

errors (when (\n -> n < 0) <| fail "error") -1
--> [ "error" ]

minBound : err -> comparable -> Validator comparable err

A constructor for Validator providing minimum bound.

errors (minBound "Too small" 10) 2
--> [ "Too small" ]

maxBound : err -> comparable -> Validator comparable err

A constructor for Validator providing maximum bound.

errors (maxBound "Too large" 100) 200
--> [ "Too large" ]

maxLength : err -> Basics.Int -> Validator String err

A constructor for Validator providing maximum length.

errors (maxLength "Too long" 10) "tooooooooo long"
--> [ "Too long" ]

minLength : err -> Basics.Int -> Validator String err

A constructor for Validator providing minimum length.

errors (minLength "Too short" 10) "short"
--> [ "Too short" ]

pattern : err -> Regex -> Validator String err

A constructor for Validator from a regular expression.

import Regex

errors (pattern "Pattern error" (Regex.fromString "^foo" |> Maybe.withDefault Regex.never)) "foobar"
--> []

errors (pattern "Pattern error" (Regex.fromString "^foo" |> Maybe.withDefault Regex.never)) "barfoo"
--> [ "Pattern error" ]

custom : (a -> Maybe err) -> Validator a err

A constructor for Validator from a function.

errors (custom (\n -> if n < 10 then Just "Too small" else Nothing)) 8
--> [ "Too small" ]

Combinators

concat : List (Validator a err) -> Validator a err

Concatnate list of validators.

import Regex

errors (concat [ minBound "Too small" 10, maxBound "Too large" 100 ]) 8
--> [ "Too small" ]

errors (concat [ minBound "Too small" 10, maxBound "Too large" 100 ]) 20
--> []

errors (concat [ minLength "Too short" 10, pattern "Does not match pattern" (Regex.fromString "^foo" |> Maybe.withDefault Regex.never) ]) "bar"
--> [ "Too short", "Does not match pattern" ]

or : Validator a err -> Validator a err -> Validator a err

Combine two validators on OR condition.

import Regex

errors
    (or
        (minLength "Too short" 10)
        (pattern "Does not match pattern"
            (Regex.fromString "^foo"
                |> Maybe.withDefault Regex.never
            )
        )
    )
    "foobar"
--> []

errors
    (or
        (minLength "Too short" 10)
        (pattern "Does not match pattern"
            (Regex.fromString "^foo"
                |> Maybe.withDefault Regex.never
            )
        )
    )
    "enough long"
--> []

errors
    (or (minLength "Too short" 10)
        (pattern "Does not match pattern"
            (Regex.fromString "^foo"
                |> Maybe.withDefault Regex.never
            )
        )
    )
    "short"
--> [ "Does not match pattern" ]

oneOf : List (Validator a err) -> Validator a err

An alternative way to combine multiple validators by OR rule. If provided list is empty, resulting validator always succeeds.

or validatorA validatorB == oneOf [ validatorA, validatorB ]

or (or validatorA validatorB) validatorC == oneOf [ validatorA, validatorB, validatorC ]

oneOf [] == succeed

Helper functions

required : err -> Validator a err -> Validator (Maybe a) err

A convenient wrapper for validating required values. It assumes input values are stored as Maybe a instead of just a. This function is just a helper function, so you could declare your own for your situation.

errors (required "Cannot be empty" <| minBound "Too small" 10) Nothing
--> [ "Cannot be empty" ]

errors (required "Cannot be empty" <| minBound "Too small" 10) <| Just 100
--> []

errors (required "Cannot be empty" <| minBound "Too small" 10) <| Just 2
--> [ "Too small" ]

optional : Validator a err -> Validator (Maybe a) err

A convenient wrapper for validating optional values. It assumes input values are stored as Maybe a instead of just a. This function is just a helper function, so you could declare your own for your situation.

errors (optional <| minLength "Too small" 10) Nothing
--> []

errors (optional <| minLength "Too small" 10) <| Just "enough long"
--> []

errors (optional <| minLength "Too small" 10) <| Just "short"
--> [ "Too small" ]

when : (a -> Basics.Bool) -> Validator a err -> Validator a err

Only checks validity if a condition is True.

import Regex

checkPrefix : Validator String String
checkPrefix = pattern "Incorrect format" (Regex.fromString "^foo" |> Maybe.withDefault Regex.never)

errors (when (\str -> String.length str > 2) checkPrefix) "ba"
--> []

errors (when (\str -> String.length str > 2) checkPrefix) "bar"
--> [ "Incorrect format" ]

unless : (a -> Basics.Bool) -> Validator a err -> Validator a err

Only checks validity unless a condition is True.

import Regex

checkPrefix : Validator String String
checkPrefix = pattern "Incorrect format" (Regex.fromString "^foo" |> Maybe.withDefault Regex.never)

errors (unless (\str -> String.length str < 3) checkPrefix) "ba"
--> []

errors (unless (\str -> String.length str < 3) checkPrefix) "bar"
--> [ "Incorrect format" ]

with : (a -> Validator a err) -> Validator a err

import Regex

checkPrefix : Validator String String
checkPrefix = pattern "Incorrect format" (Regex.fromString "^foo" |> Maybe.withDefault Regex.never)

type alias Form =
    { foo : Maybe String
    , isRequired : Bool
    }

form : Validator Form String
form =
    with <| \{ isRequired } ->
        lift .foo <|
            (if isRequired then required "Required" else optional)
                checkPrefix

errors form { foo = Nothing, isRequired = False }
--> []

errors form { foo = Nothing, isRequired = True }
--> [ "Required" ]

errors form { foo = Just "bar" , isRequired = True }
--> [ "Incorrect format" ]

Operators

map : (suberr -> err) -> Validator a suberr -> Validator a err

Convert err type.

lift : (a -> b) -> Validator b err -> Validator a err

lift is mainly used for accessing sub model of target value.

errors (lift .str <| minLength "Too short" 10) { str = "foo", int = 5 }
--> [ "Too short" ]

liftMap : (suberr -> err) -> (a -> b) -> Validator b suberr -> Validator a err

liftMap can convert a validator by lift and map at one time for convenience.