dwayne/elm-field/1.0.0

Field

Field

type alias Field a =
Field Error a

Tracks the raw String input and either the value of type a that the String represents or the errors of type Error that can accumulate.

Type

type alias Type a =
Type Error a

Represents the type information for a field. It specifies how a String is converted to a type a and back to a String. Conversion may result in errors of type Error.

Primitive

Elm's primitive types: Int, Float, Bool, Char, and String, all have corresponding field types.

Int

int : Type Int

Any Int that can be parsed from a trimmed string using String.toInt.

(typeToConverters int).fromString "-1" == Ok -1


(typeToConverters int).fromString "0" == Ok 0


(typeToConverters int).fromString "1" == Ok 1


(typeToConverters int).fromString " 5 " == Ok 5


(typeToConverters int).fromString "" == Err blankError


(typeToConverters int).fromString "five" == Err (syntaxError "five")
nonNegativeInt : Type Int

Any Int, n, that can be parsed from a trimmed string using String.toInt such that n >= 0.

(typeToConverters nonNegativeInt).fromString "-1" == Err (validationError "-1")


(typeToConverters nonNegativeInt).fromString "0" == Ok 0


(typeToConverters nonNegativeInt).fromString "1" == Ok 1


(typeToConverters nonNegativeInt).fromString " 5 " == Ok 5


(typeToConverters nonNegativeInt).fromString "" == Err blankError


(typeToConverters nonNegativeInt).fromString "five" == Err (syntaxError "five")
positiveInt : Type Int

Any Int, n, that can be parsed from a trimmed string using String.toInt such that n > 0.

(typeToConverters positiveInt).fromString "-1" == Err (validationError "-1")


(typeToConverters positiveInt).fromString "0" == Err (validationError "0")


(typeToConverters positiveInt).fromString "1" == Ok 1


(typeToConverters positiveInt).fromString " 5 " == Ok 5


(typeToConverters positiveInt).fromString "" == Err blankError


(typeToConverters positiveInt).fromString "five" == Err (syntaxError "five")
nonPositiveInt : Type Int

Any Int, n, that can be parsed from a trimmed string using String.toInt such that n <= 0.

(typeToConverters nonPositiveInt).fromString "-1" == Ok -1


(typeToConverters nonPositiveInt).fromString "0" == Ok 0


(typeToConverters nonPositiveInt).fromString "1" == Err (validationError "1")


(typeToConverters nonPositiveInt).fromString " 5 " == Err (validationError "5")


(typeToConverters nonPositiveInt).fromString "" == Err blankError


(typeToConverters nonPositiveInt).fromString "five" == Err (syntaxError "five")
negativeInt : Type Int

Any Int, n, that can be parsed from a trimmed string using String.toInt such that n < 0.

(typeToConverters negativeInt).fromString "-1" == Ok -1


(typeToConverters negativeInt).fromString "0" == Err (validationError "0")


(typeToConverters negativeInt).fromString "1" == Err (validationError "1")


(typeToConverters negativeInt).fromString " 5 " == Err (validationError "5")


(typeToConverters negativeInt).fromString "" == Err blankError


(typeToConverters negativeInt).fromString "five" == Err (syntaxError "five")
subsetOfInt : (Int -> Bool) -> Type Int

Any Int, n, that can be parsed from a trimmed string using String.toInt such that isGood n is True.

evens = subsetOfInt (modBy 2 >> (==) 0)


(typeToConverters evens).fromString "-2" == Ok -2


(typeToConverters evens).fromString "-1" == Err (validationError "-1")


(typeToConverters evens).fromString "0" == Ok 0


(typeToConverters evens).fromString "1" == Err (validationError "1")


(typeToConverters evens).fromString " 2 " == Ok 2


(typeToConverters evens).fromString "" == Err blankError


(typeToConverters evens).fromString "five" == Err (syntaxError "five")

Float

float : Type Float

Any Float that can be parsed from a trimmed string using String.toFloat.

(typeToConverters float).fromString "-0.1" == Ok -0.1


(typeToConverters float).fromString "0" == Ok 0


(typeToConverters float).fromString "1.1" == Ok 1.1


(typeToConverters float).fromString " 3.14 " == Ok 3.14


(typeToConverters float).fromString "" == Err blankError


(typeToConverters float).fromString "pi" == Err (syntaxError "pi")
nonNegativeFloat : Type Float

Any Float, f, that can be parsed from a trimmed string using String.toFloat such that f >= 0.

(typeToConverters nonNegativeFloat).fromString "-0.1" == Err (validationError "-0.1")


(typeToConverters nonNegativeFloat).fromString "0" == Ok 0


(typeToConverters nonNegativeFloat).fromString "1.1" == Ok 1.1


(typeToConverters nonNegativeFloat).fromString " 3.14 " == Ok 3.14


(typeToConverters nonNegativeFloat).fromString "" == Err blankError


(typeToConverters nonNegativeFloat).fromString "pi" == Err (syntaxError "pi")
positiveFloat : Type Float

Any Float, f, that can be parsed from a trimmed string using String.toFloat such that f > 0.

(typeToConverters positiveFloat).fromString "-0.1" == Err (validationError "-0.1")


(typeToConverters positiveFloat).fromString "0" == Err (validationError "0")


(typeToConverters positiveFloat).fromString "1.1" == Ok 1.1


(typeToConverters positiveFloat).fromString " 3.14 " == Ok 3.14


(typeToConverters positiveFloat).fromString "" == Err blankError


(typeToConverters positiveFloat).fromString "pi" == Err (syntaxError "pi")
nonPositiveFloat : Type Float

Any Float, f, that can be parsed from a trimmed string using String.toFloat such that f <= 0.

(typeToConverters nonPositiveFloat).fromString "-0.1" == Ok -0.1


(typeToConverters nonPositiveFloat).fromString "0" == Ok 0


(typeToConverters nonPositiveFloat).fromString "1.1" == Err (validationError "1.1")


(typeToConverters nonPositiveFloat).fromString " 3.14 " == Err (validationError "3.14")


(typeToConverters nonPositiveFloat).fromString "" == Err blankError


(typeToConverters nonPositiveFloat).fromString "pi" == Err (syntaxError "pi")
negativeFloat : Type Float

Any Float, f, that can be parsed from a trimmed string using String.toFloat such that f < 0.

(typeToConverters negativeFloat).fromString "-0.1" == Ok -0.1


(typeToConverters negativeFloat).fromString "0" == Err (validationError "0")


(typeToConverters negativeFloat).fromString "1.1" == Err (validationError "1.1")


(typeToConverters negativeFloat).fromString " 3.14 " == Err (validationError "3.14")


(typeToConverters negativeFloat).fromString "" == Err blankError


(typeToConverters negativeFloat).fromString "pi" == Err (syntaxError "pi")
subsetOfFloat : (Float -> Bool) -> Type Float

Any Float, f, that can be parsed from a trimmed string using String.toFloat such that isGood f is True.

roundsToTwo = subsetOfFloat (round >> (==) 2)


(typeToConverters roundsToTwo).fromString "1.3" == Err (validationError "1.3")


(typeToConverters roundsToTwo).fromString "1.5" == Ok 1.5


(typeToConverters roundsToTwo).fromString "1.8" == Ok 1.8


(typeToConverters roundsToTwo).fromString "2.0" == Ok 2.0


(typeToConverters roundsToTwo).fromString "2.3" == Ok 2.3


(typeToConverters roundsToTwo).fromString "2.5" == Err (validationError "2.5")


(typeToConverters roundsToTwo).fromString "3.14" == Err (validationError "3.14")


(typeToConverters roundsToTwo).fromString "" == Err blankError


(typeToConverters roundsToTwo).fromString "pi" == Err (syntaxError "pi")

Bool

bool : Type Bool

Any case-insensitive defaultTruthy or defaultFalsy trimmed string.

(typeToConverters bool).fromString "true" == Ok True


(typeToConverters bool).fromString "TrUe" == Ok True


(typeToConverters bool).fromString "false" == Ok False


(typeToConverters bool).fromString "fAlSe" == Ok False


(typeToConverters bool).fromString " 1 " == Ok True


(typeToConverters bool).fromString " 0 " == Ok False


(typeToConverters bool).fromString "" == Err blankError


(typeToConverters bool).fromString "not" == Err (syntaxError "not")
true : Type Bool

Any case-insensitive defaultTruthy trimmed string.

(typeToConverters true).fromString "true" == Ok True


(typeToConverters true).fromString "TrUe" == Ok True


(typeToConverters true).fromString "false" == Err (validationError "false")


(typeToConverters true).fromString "fAlSe" == Err (validationError "false")


(typeToConverters true).fromString " 1 " == Ok True


(typeToConverters true).fromString " 0 " == Err (validationError "false")


(typeToConverters true).fromString "" == Err blankError


(typeToConverters true).fromString "not" == Err (syntaxError "not")
false : Type Bool

Any case-insensitive defaultFalsy trimmed string.

(typeToConverters false).fromString "true" == Err (validationError "true")


(typeToConverters false).fromString "TrUe" == Err (validationError "true")


(typeToConverters false).fromString "false" == Ok False


(typeToConverters false).fromString "fAlSe" == Ok False


(typeToConverters false).fromString " 1 " == Err (validationError "true")


(typeToConverters false).fromString " 0 " == Ok False


(typeToConverters false).fromString "" == Err blankError


(typeToConverters false).fromString "not" == Err (syntaxError "not")
customSubsetOfBool : CustomBoolOptions -> (Bool -> Bool) -> Type Bool

Customize the options.

import Set


mySubsetOfBool : (Bool -> Bool) -> Type Bool
mySubsetOfBool =
    customSubsetOfBool
        { truthy = Set.fromList [ "#t" ]
        , falsy = Set.fromList [ "#f" ]
        , toString =
            \b ->
                if b then
                    "#t"


                else
                    "#f"
        , caseSensitive = True
        }


myBool : Type Bool
myBool =
    mySubsetOfBool (always True)


myTrue : Type Bool
myTrue =
    mySubsetOfBool ((==) True)


myFalse : Type Bool
myFalse =
    mySubsetOfBool ((==) False)
type alias CustomBoolOptions =
CustomBoolOptions

Used by customSubsetOfBool to customize the options.

defaultCustomBoolOptions : CustomBoolOptions

The default options used by bool, true, and false.

{ truthy = defaultTruthy
, falsy = defaultFalsy
, toString = defaultBoolToString
, caseSensitive = False
}
defaultTruthy : Set String

The default truthy strings: "true", "1", "yes", "on", "y", and "enabled".

defaultFalsy : Set String

The default falsy strings: "false", "0", "no", "off", "n", and "disabled".

defaultBoolToString : Bool -> String
defaultBoolToString True == "true"


defaultBoolToString False == "false"

Char

char : Type Char

Any Char that can be parsed from a string of exactly length one. The string is not trimmed.

(typeToConverters char).fromString "a" == Ok 'a'


(typeToConverters char).fromString "A" == Ok 'A'


(typeToConverters char).fromString "8" == Ok '8'


(typeToConverters char).fromString "\n" == Ok '\n'


(typeToConverters char).fromString "" == Err blankError


(typeToConverters char).fromString " " == Ok ' '


(typeToConverters char).fromString "  " == Err blankError


(typeToConverters char).fromString "aA" == Err (syntaxError "aA")


(typeToConverters char).fromString " 2 " == Err (syntaxError " 2 ")
subsetOfChar : (Char -> Bool) -> Type Char

Any Char, c, that can be parsed from a string of exactly length one such that isGood c is True. The string is not trimmed.

digit = subsetOfChar Char.isDigit


(typeToConverters digit).fromString "a" == Err (validationError "a")


(typeToConverters digit).fromString "A" == Err (validationError "A")


(typeToConverters digit).fromString "8" == Ok '8'


(typeToConverters digit).fromString "\n" == Err (validationError "\n")


(typeToConverters digit).fromString "" == Err blankError


(typeToConverters digit).fromString " " == Err (validationError " ")


(typeToConverters digit).fromString "  " == Err blankError


(typeToConverters digit).fromString "aA" == Err (syntaxError "aA")


(typeToConverters digit).fromString " 2 " == Err (syntaxError " 2 ")

String

A blank string is either the empty string or a non-empty string consisting entirely of whitespace characters.

The empty string: ""

Some blank strings: "", " ", " ", " \t ", "\n \t \r"

string : Type e String

Any trimmed String is accepted. It never fails.

(typeToConverters string).fromString "Hello" == Ok "Hello"


(typeToConverters string).fromString " Hello " == Ok "Hello"


(typeToConverters string).fromString "" == Ok ""


(typeToConverters string).fromString " \n\t " == Ok ""
subsetOfString : (String -> Bool) -> Type String

Any trimmed String, s, is accepted if isGood s is True.

atMost3 = subsetOfString (String.length >> (>=) 3)


(typeToConverters atMost3).fromString "p" == Ok "p"


(typeToConverters atMost3).fromString "pi" == Ok "pi"


(typeToConverters atMost3).fromString "pie" == Ok "pie"


(typeToConverters atMost3).fromString " pie " == Ok "pie"


(typeToConverters atMost3).fromString "Hello" == Err (validationError "Hello")


(typeToConverters atMost3).fromString " Hello " == Err (validationError "Hello")


(typeToConverters atMost3).fromString "" == Ok ""


(typeToConverters atMost3).fromString " \n\t " == Ok ""
nonEmptyString : Type String

Any String except the empty string is accepted. The string is not trimmed.

(typeToConverters nonEmptyString).fromString "Hello" == Ok "Hello"


(typeToConverters nonEmptyString).fromString " Hello " == Ok " Hello "


(typeToConverters nonEmptyString).fromString "" == Err blankError


(typeToConverters nonEmptyString).fromString " \n\t " == Ok " \n\t "
subsetOfNonEmptyString : (String -> Bool) -> Type String

Any non-empty String, s, is only accepted if isGood s is True. The string is not trimmed.

hello = subsetOfNonEmptyString ((==) "Hello")


(typeToConverters hello).fromString "Hello" == Ok "Hello"


(typeToConverters hello).fromString " Hello " == Err (validationError " Hello ")


(typeToConverters hello).fromString "" == Err blankError


(typeToConverters hello).fromString " \n\t " == Err (validationError " \n\t ")
nonBlankString : Type String

Any non-blank trimmed String is accepted.

(typeToConverters nonBlankString).fromString "Hello" == Ok "Hello"


(typeToConverters nonBlankString).fromString " Hello " == Ok "Hello"


(typeToConverters nonBlankString).fromString "" == Err blankError


(typeToConverters nonBlankString).fromString " \n\t " == Err blankError
subsetOfNonBlankString : (String -> Bool) -> Type String

Any non-blank trimmed String, s, is accepted if isGood s is True.

atMost3 = subsetOfNonBlankString (String.length >> (>=) 3)


(typeToConverters atMost3).fromString "p" == Ok "p"


(typeToConverters atMost3).fromString "pi" == Ok "pi"


(typeToConverters atMost3).fromString "pie" == Ok "pie"


(typeToConverters atMost3).fromString " pie " == Ok "pie"


(typeToConverters atMost3).fromString "Hello" == Err (validationError "Hello")


(typeToConverters atMost3).fromString " Hello " == Err (validationError "Hello")


(typeToConverters atMost3).fromString "" == Err blankError


(typeToConverters atMost3).fromString " \n\t " == Err blankError

User-defined

subsetOfType : (a -> Bool) -> Type a -> Type a

Accept a subset of the values accepted by the given type.

positiveEvens = subsetOfType (modBy 2 >> (==) 0) positiveInt


(typeToConverters positiveEvens).fromString " 2 " == Ok 2


(typeToConverters positiveEvens).fromString "-2" == Err (validationError "-2")


(typeToConverters positiveEvens).fromString "-1" == Err (validationError "-1")


(typeToConverters positiveEvens).fromString "0" == Err (validationError "0")


(typeToConverters positiveEvens).fromString "1" == Err (validationError "1")


(typeToConverters positiveEvens).fromString "" == Err blankError


(typeToConverters positiveEvens).fromString "five" == Err (syntaxError "five")
{ fromString : String -> Result Error a
, toString : a -> String
}
-> Type a

Define a new field type.

type Email = Email String


email : Type Email
email =
    customType
        { fromString =
            trim
                (\s ->
                    if s |> String.contains "@" then
                        Ok (Email s)
                    else
                        Err (syntaxError s)
                )
        , toString =
            \(Email s) -> s
        }


(typeToConverters email).fromString "[email protected]" == Ok (Email "[email protected]")


(typeToConverters email).fromString " [email protected] " == Ok (Email "[email protected]")


(typeToConverters email).fromString "" == Err blankError


(typeToConverters email).fromString " ab.c " == Err (syntaxError "ab.c")

Optional

optional : Type a -> Type (Maybe a)

Make any field type optional.

maybePositiveInt = optional positiveInt


(typeToConverters maybePositiveInt).fromString "-1" == Err (validationError "-1")


(typeToConverters maybePositiveInt).fromString "0" == Err (validationError "0")


(typeToConverters maybePositiveInt).fromString "1" == Ok (Just 1)


(typeToConverters maybePositiveInt).fromString " 5 " == Ok (Just 5)


(typeToConverters maybePositiveInt).fromString "" == Ok Nothing


(typeToConverters maybePositiveInt).fromString "five" == Err (syntaxError "five")

Construct

All newly constructed fields are clean.

empty : Type a -> Field a

Initialize a new field from the given type and the empty string.

For all field types, t, empty t is equivalent to fromString t "".

fromString : Type a -> String -> Field a

Initialize a new field from the given type and string.

toMaybe (fromString bool "enabled") == Just True
fromValue : Type a -> a -> Field a

Initialize a new field from the given type and value.

toMaybe (fromValue char 'a') == Just 'a'

Change

Any change to a field using these setter functions makes the field dirty. Furthermore, these are the only functions that can dirty a field.

setFromString : String -> Field a -> Field a
field = empty positiveInt


toResult field == Err [ blankError ]


toResult (setFromString "-1" field) == Err [ validationError "-1" ]


toResult (setFromString "0" field) == Err [ validationError "0" ]


toResult (setFromString "1" field) == Ok 1


toResult (setFromString " 5 " field) == Ok 5


toResult (setFromString "   " field) == Err [ blankError ]


toResult (setFromString "five" field) == Err [ syntaxError "five" ]
setFromValue : a -> Field a -> Field a
field = empty (optional positiveInt)


toResult field == Ok Nothing


toResult (setFromValue (Just -1) field) == Err [ validationError "-1" ]


toResult (setFromValue (Just 0) field) == Err [ validationError "0" ]


toResult (setFromValue (Just 1) field) == Ok (Just 1)


toResult (setFromValue (Just 5) field) == Ok (Just 5)


toResult (setFromValue Nothing field) == Ok Nothing
setError : Error -> Field a -> Field a
field = empty int


error = customError "Not an integer"


toResult (setError error field) == Err [ error ]
setErrors : Error -> List Error -> Field a -> Field a
setCustomError : String -> Field a -> Field a
field = empty int


error = customError "Not an integer"


toResult (setCustomError "Not an integer" field) == Err [ error ]
setCustomErrors : String -> List String -> Field a -> Field a

Query

isEmpty : Field a -> Bool

Determine whether or not the raw string is equal to the empty string.

isEmpty (empty int) == True


isEmpty (fromString int "   ") == False


isEmpty (fromString int "1") == False
isNonEmpty : Field a -> Bool

isNonEmpty f is equivalent to not (isEmpty f).

isNonEmpty (empty int) == False


isNonEmpty (fromString int "   ") == True


isNonEmpty (fromString int "1") == True
isBlank : Field a -> Bool

Determine whether or not the raw string is equal to a blank string.

isBlank (empty int) == True


isBlank (fromString int "   ") == True


isBlank (fromString int "1") == False
isNonBlank : Field a -> Bool

isNonBlank f is equivalent to not (isBlank f).

isNonBlank (empty int) == False


isNonBlank (fromString int "   ") == False


isNonBlank (fromString int "1") == True
isClean : Field a -> Bool

True if the field is clean.

isClean (empty int) == True


isClean (fromString int "1") == True


isClean (setFromString "1" (empty int)) == False
isDirty : Field a -> Bool

True if the field is dirty. isDirty f is equivalent to not (isClean f).

isDirty (empty int) == False


isDirty (fromString int "1") == False


isDirty (setFromString "1" (empty int)) == True
isValid : Field a -> Bool

True if the field does not have any errors.

isValid (empty int) == False


isValid (fromString int "1") == True
isInvalid : Field a -> Bool

True if the field has errors. isInvalid f is equivalent to not (isValid f).

isInvalid (empty int) == True


isInvalid (fromString int "1") == False

Convert

toRawString : Field a -> String
toRawString (empty int) == ""


toRawString (fromString int " \n5  \t \n") == " \n5  \t \n"
toString : Field a -> String
toString (empty int) == ""


toString (fromString int " \n5  \t \n") == "5"
toMaybe : Field a -> Maybe a
toMaybe (empty int) == Nothing


toMaybe (fromString int "5") == Just 5
toResult : Field a -> Result (List Error) a
toResult (empty int) == Err [ blankError ]


toResult (fromString int "5") == Ok 5
toValidation : Field a -> Validation Error a
import Validation as V


toValidation (empty int) == V.fail blankError


toValidation (fromString int "5") == V.succeed 5

Validation

type alias Validation e a =
Validation e a

Re-export the Validation type for convenience.

succeed : a -> Validation e a

Useful to use with applyValidation.

Let f represent a function that takes N >= 1 arguments. Then,

succeed f
    |> applyValidation field1
    |> applyValidation field2
    |> ...
    |> applyValidation fieldN

applies f to N arguments as long as no field has an error.

validationToResult : Validation e a -> Result (List e) a

Convert from a Validaton to a Result for convenience.

Useful to use with applyValidation.

Let f represent a function that takes N >= 1 arguments. Then,

succeed f
    |> applyValidation field1
    |> applyValidation field2
    |> ...
    |> applyValidation fieldN
    |> validationToResult

applies f to N arguments as long as no field has an error.

Converters

type alias Converters a =
Converters Error a

The collection of conversion functions that comprise a Type. The conversion functions are useful for building new field types from existing field types.

type Positive
    = Positive Int


fromString : String -> Result Error Positive
fromString =
    (typeToConverters positiveInt).fromString >> Result.map Positive


toString : Positive -> String
toString (Positive p) =
    (typeToConverters positiveInt).toString p


fieldType : Type Positive
fieldType =
    customType
        { fromString = fromString
        , toString = toString
        }
typeToConverters : Type a -> Converters a
toConverters : Field a -> Converters a

Validate

validate : (a -> value) -> Field x a -> Validation x value

Apply a function to the processed value of a field. If the field is valid, the value will be converted. If the field is invalid, the same error will be propagated.

import Validation as V


validate sqrt (fromString float "4.0") == V.succeed 2.0


validate sqrt (fromString float "") == V.fail blankError
validate2 : (a -> b -> value) -> Field x a -> Field x b -> Validation x value

Apply a function if both fields are valid. If not, the errors will be accumulated in order of failure.

import Validation as V


validate2 max (fromString int "42") (fromString int "13") == V.succeed 42


validate2 max (fromString int "x") (fromString int "13") == V.fail (syntaxError "x")


validate2 max (fromString int "42") (fromString int "y") == V.fail (syntaxError "y")


validate2 max (fromString int "x") (fromString int "y") == V.failWithErrors (syntaxError "x") [ syntaxError "y" ]
(a -> b -> c -> value)
-> Field x a
-> Field x b
-> Field x c
-> Validation x value

Apply a function if all three fields are valid. If not, the errors will be accumulated in order of failure.

validate3 (\a b c -> a + b + c) (fromString int "1") (fromString int "2") (fromString int "3") == V.succeed 6


validate3 (\a b c -> a + b + c) (fromString int "1") (fromString int "y") (fromString int "3") == V.fail (syntaxError "y")


validate3 (\a b c -> a + b + c) (fromString int "x") (fromString int "2") (fromString int "z") == V.failWithErrors (syntaxError "x") [ syntaxError "z" ]


validate3 (\a b c -> a + b + c) (fromString int "x") (fromString int "y") (fromString int "z") == V.failWithErrors (syntaxError "x") [ syntaxError "y", syntaxError "z" ]
(a -> b -> c -> d -> value)
-> Field x a
-> Field x b
-> Field x c
-> Field x d
-> Validation x value
(a -> b -> c -> d -> e -> value)
-> Field x a
-> Field x b
-> Field x c
-> Field x d
-> Field x e
-> Validation x value

Apply

Field e is not applicative. However, Maybe, Result e, and Validaton e are all applicative. The functions below attempt to make it convenient to do an applicative style of programming with fields by leveraging the applicative nature of Maybe, Result e, and Validation e.

applyMaybe : Field a -> Maybe (a -> b) -> Maybe b

Suppose you want to use the values from multiple fields but you don't care about any of the errors that are present.

type Primitives
    = Positive Int Float Bool Char String
    | NonPositive Int Float Bool Char String


let
    maybePrimitives =
        (\n f b c s ->
            if n > 0 then
                Positive n f b c s
            else
                NonPositive n f b c s
        )
        |> Just
        |> applyMaybe (fromString int "5")
        |> applyMaybe (fromString float "3.14")
        |> applyMaybe (fromValue bool True)
        |> applyMaybe (fromValue char 'a')
        |> applyMaybe (fromString string "Hello")
in
maybePrimitives == Just (Positive 5 3.14 True 'a' "Hello")
applyResult : Field e a -> Result (List e) (a -> b) -> Result (List e) b

Suppose you want to use the values from multiple fields and you only care about the first error that occurs.

type Primitives
    = Positive Int Float Bool Char String
    | NonPositive Int Float Bool Char String


let
    resultPrimitives =
        (\n f b c s ->
            if n > 0 then
                Positive n f b c s
            else
                NonPositive n f b c s
        )
        |> Ok
        |> applyResult (fromString int "5")
        |> applyResult (fromString float "pi")
        |> applyResult (fromString bool "not")
        |> applyResult (fromValue char 'a')
        |> applyResult (fromString string "Hello")
in
resultPrimitives == Err [ syntaxError "pi" ]
applyValidation : Field e a -> Validation e (a -> b) -> Validation e b

Suppose you want to use the values from multiple fields and you care about all the errors that occur.

type Primitives
    = Positive Int Float Bool Char String
    | NonPositive Int Float Bool Char String


let
    resultPrimitives =
        (\n f b c s ->
            if n > 0 then
                Positive n f b c s
            else
                NonPositive n f b c s
        )
        |> succeed
        |> applyValidation (fromString int "5")
        |> applyValidation (fromString float "pi")
        |> applyValidation (fromString bool "not")
        |> applyValidation (fromValue char 'a')
        |> applyValidation (fromString string "Hello")
        |> validationToResult
in
resultPrimitives == Err [ syntaxError "pi", syntaxError "not" ]

String Helpers

trim : (String -> Result Error a) -> String -> Result Error a

Get rid of whitespace on both sides of a string.

If the trimmed string is empty return blankError, otherwise apply the given function to the trimmed string.

It is useful when you're defining a custom field type.

type Username = Username String


username : Type Username
username =
    customType
        { fromString =
            trim
                (\s ->
                    let
                        len =
                            String.length s
                    in
                    if len < 3 then
                        Err <| customError "The username must have at least 3 characters"
                    else if len > 25 then
                        Err <| customError "The username must have at most 25 characters"
                    else
                        Ok (Username s)
                )
        , toString =
            \(Username s) -> s
        }




toResult (fromString username "abc") == Ok (Username "abc")


toResult (fromString username "ab") == Err [ customError "The username must have at least 3 characters" ]


toResult (fromString username "abcdefghijklmnopqrstuvwxyz") == Err [ customError "The username must have at most 25 characters" ]

Error

type alias Error =
Error String

The error type returned by the built-in field types. It supports blank, syntax, validation, and custom validation errors.

blankError : Error

Indicates that the user's input string is either the empty string or a string containing only whitespace characters.

toResult (fromString positiveInt "") == Err [ blankError ]


toResult (fromString positiveInt "\n\t ") == Err [ blankError ]
syntaxError : String -> Error

Indicates that the user's input string cannot be converted to the desired type.

toResult (fromString positiveInt "five") == Err [ syntaxError "five" ]

The string used in the syntax error is the trimmed user's input string that caused the error.

toResult (fromString positiveInt " five\n \t") == Err [ syntaxError "five" ]
validationError : String -> Error

Indicates that the user's input string was successfully converted to a value of the desired type but that the value wasn't contained in the subset of values under consideration.

toResult (fromString positiveInt "0") == Err [ validationError "0" ]


toResult (fromString positiveInt "-1") == Err [ validationError "-1" ]

Let v represent the value to which the user's input string was successfully converted. The string used in the validation error is the string returned by the toString function of the field's type when it's applied to v.

v : Int
v = -1


field : Field Int
field = fromString positiveInt " -1 \n \t"


negativeOne : String
negativeOne = (toConverters field).toString v


toResult field == Err [ validationError negativeOne ]

In the above example, the user's input string, " -1 \n \t", would be successfully converted to the value, -1, but -1 is not a positive integer. Hence, the string representation of -1 as determined by (toConverters field).toString is used in the validation error.

customError : String -> Error

Indicates that the user's input string was successfully converted to a value of the desired type but that the value wasn't contained in the subset of values under consideration.

It's similar to a validationError but the string it takes can be a user-friendly error message.

type Username = Username String


username : Type Username
username =
    customType
        { fromString =
            trim
                (\s ->
                    let
                        len =
                            String.length s
                    in
                    if len < 3 then
                        Err (customError <| "The username must have at least 3 characters: " ++ String.fromInt len)
                    else if len > 25 then
                        Err (customError <| "The username must have at most 25 characters: " ++ String.fromInt len)
                    else
                        Ok (Username s)
                )
        , toString =
            \(Username s) -> s
        }


toResult (fromString username "abc") == Ok (Username "abc")


toResult (fromString username "ab") == Err [ customError "The username must have at least 3 characters: 2" ]


toResult (fromString username "abcdefghijklmnopqrstuvwxyz") == Err [ customError "The username must have at most 25 characters: 26" ]
{ onBlank : String
, onSyntaxError : String -> String
, onValidationError : String -> String
}
-> Error
-> String

Convert your errors to user-friendly error messages.

toErrorMessage : Error -> String
toErrorMessage =
    errorToString
        { onBlank = "It is required"
        , onSyntaxError = (++) "It is incorrectly formatted: "
        , onValidationError = (++) "It is invalid: "
        }


toErrorMessage blankError == "It is required"


toErrorMessage (syntaxError "five") == "It is incorrectly formatted: five"


toErrorMessage (validationError "-1") == "It is invalid: -1"


toErrorMessage (customError "The username must have at least 3 characters: 2") == "The username must have at least 3 characters: 2"

Handle Errors

mapError : (x -> y) -> Field x a -> Field y a

Apply a function to the error value of the field type. It's useful for unifying multiple custom field errors under a single error type.

Let's continue with the example from customError and incorporate an Age type.

type Age = Age Int


age : Type Age
age =
    customType
        { fromString =
            (typeToConverters positiveInt).fromString
                >> Result.andThen
                    (\n ->
                        if n < 21 then
                            Err (customError <| "The age must be at least 21 years: " ++ String.fromInt n)
                        else if n > 65 then
                            Err (customError <| "The age must be at most 65 years: " ++ String.fromInt n)
                        else
                            Ok (Age n)
                    )
        , toString =
            \(Age n) -> String.fromInt n
        }


--
-- 1. The single error type under which we will unify the username and age field errors.
--
type Errors
    = MyUsernameError Error
    | MyAgeError Error


usernameField : Field Username
usernameField = fromString username "ab"


ageField : Field Age
ageField = fromString age "67"


type alias UserDetails = { username : Username, age : Age }


--
-- 2. We use mapError in order to validate both fields at the same time.
--
resultUserDetails =
    validationToResult <|
        validate2
            UserDetails
            (usernameField |> mapError MyUsernameError)
            (ageField |> mapError MyAgeError)


resultUserDetails == Err [ MyUsernameError (customError "The username must have at least 3 characters: 2"), MyAgeError (customError "The age must be at most 65 years: 67") ]
firstError : Field a -> Maybe Error
fromString string "A string"
    |> firstError
    |> (==) Nothing


fromString string "A string"
    |> setCustomErrors "First error" [ "Second error", "Third error" ]
    |> firstError
    |> (==) (Just <| customError "First error")
lastError : Field a -> Maybe Error
fromString string "A string"
    |> lastError
    |> (==) Nothing


fromString string "A string"
    |> setCustomErrors "First error" [ "Second error", "Third error" ]
    |> lastError
    |> (==) (Just <| customError "Third error")
allErrors : Field a -> List Error
fromString string "A string"
    |> allErrors
    |> (==) []


fromString string "A string"
    |> setCustomErrors "First error" [ "Second error", "Third error" ]
    |> allErrors
    |> (==) [ customError "First error", customError "Second error", customError "Third error" ]