obiloud / validator-pipeline / Validator

This library helps with building custom validators of a user input. Many validators can be chained to perform validation of a single value. Provides interface for combining validatiors of multiple input fields with (|>) operator. Allows combination of required and optional fields.

Definition


type Validator a e b

Represents a validator of the input value. Validation can fail with a list of custom error messages.

custom : (a -> Result (List e) b) -> Validator a e b

Build a custom validator.

intValidator : Validator String InputError Int
intValidator =
    Validator.custom (String.toInt >> Result.fromMaybe [ NaN ])

run : Validator a e b -> a -> Result (List e) b

Run validator.

intValidator : Validator String String Int
intValidator =
    Validator.custom (String.toInt >> Result.fromMaybe [ "Not a number" ])

Validator.run intValidator "42"
--> (Ok 42)

Validator.run intValidator "Fat fingers"
--> (Err [ "Not a number" ])

succeed : b -> Validator a e b

Create validator that always succeed.

Validator.run (Validator.succeed 3) "blah" --> (Ok 3)

fail : e -> Validator a e b

Create validator that always fails.

Validator.run (Validator.fail "Invalid") 123 --> (Err [ "Invalid" ])

Transform

map : (b -> c) -> Validator a e b -> Validator a e c

Transform validated value with a given function.

Validator.run (Validator.map ((+) 1) (Validator.succeed 2)) Nothing
--> (Ok 3)

map2 : (b -> c -> d) -> Validator a e b -> Validator a e c -> Validator a e d

Apply a function if both arguments passed validation.

Validator.map2 (\x y -> x + y)
    (Validator.succeed 1)
    (Validator.succeed 2)
    |> (\validator -> Validator.run validator ())
    --> (Ok 3)

Applicative

andMap : Validator a e b -> Validator a e (b -> c) -> Validator a e c

Apply wrapped function to another wrapped value.

intValidator : Validator String String Int
intValidator =
    Validator.custom (String.toInt >> Result.fromMaybe [ "Not a number" ])

Validator.run (Validator.succeed ((+) 2) |> Validator.andMap intValidator) "3"
--> (Ok 5)

andThen : (b -> Validator a e c) -> Validator a e b -> Validator a e c

Chain together many validators.

intValidator
    |> Validator.andThen
        (\x ->
            if x < 0 then
                Validator.fail "Must be a positive number"

            else
                Validator.succeed x
        )
    -- ...

Compose

required : (a -> b) -> (b -> Basics.Bool) -> e -> Validator b e c -> Validator a e (c -> d) -> Validator a e d

Combine validators with required field.

type alias User =
    { name : String
    , age : Int
    , email : Email
    }

type Email = Email String

parseEmail : String -> Result String Email
parseEmail str =
    if String.contains "@" str then
        Ok (Email str)
    else
        Err "Invalid email"

emailValidator : Validator String String Email
emailValidator =
    Validator.custom (parseEmail >> Result.mapError List.singleton)

intValidator : Validator String String Int
intValidator =
    Validator.custom (String.toInt >> Result.fromMaybe [ "Not a number" ])

type alias FormValues r =
    { r
        | name : String
        , age : String
        , email : String
    }

userValidator : Validator (FormValues r) String User
userValidator =
    Validator.succeed User
        |> Validator.required .name String.isEmpty "Name is required" (Validator.custom Ok)
        |> Validator.required .age String.isEmpty "Age is required" intValidator
        |> Validator.required .email String.isEmpty "Email is required" emailValidator

Validator.run userValidator { name = "John Doe", age = "", email = "test" }
    --> Err [ "Age is required", "Invalid email" ]

optional : (a -> b) -> (b -> Basics.Bool) -> c -> Validator b e c -> Validator a e (c -> d) -> Validator a e d

Combine validators with optional field.

type alias User =
    { name : String
    , age : Int
    , email : Email
    }

type Email = Email String

parseEmail : String -> Result String Email
parseEmail str =
    if String.contains "@" str then
        Ok (Email str)
    else
        Err "Invalid email"

emailValidator : Validator String String Email
emailValidator =
    Validator.custom (parseEmail >> Result.mapError List.singleton)

intValidator : Validator String String Int
intValidator =
    Validator.custom (String.toInt >> Result.fromMaybe [ "Not a number" ])

type alias FormValues r =
    { r
        | name : String
        , age : String
        , email : String
    }

userValidator : Validator (FormValues r) String User
userValidator =
    Validator.succeed User
        |> Validator.required .name String.isEmpty "Name is required" (Validator.custom Ok)
        |> Validator.optional .age String.isEmpty 10 intValidator
        |> Validator.required .email String.isEmpty "Email is required" emailValidator

Validator.run userValidator { name = "John Doe", age = "", email = "happy@path" }
    --> Ok (User "John Doe" 10 (Email "happy@path"))