dwayne/elm-field/1.0.0

Field.Advanced

Field

type Field e a

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

Type

type Type e 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 e.

Primitive

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

Int

int : Type (Error e) 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")

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")

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")

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")

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 (Error e) 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")
{ blankError : e
, syntaxError : String -> e
, validationError : String -> e
}
-> (Int -> Bool)
-> Type e Int

Similar to subsetOfInt but you get to customize the errors.

Float

float : Type (Error e) 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")

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")

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")

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")

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 (Error e) 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")
{ blankError : e
, syntaxError : String -> e
, validationError : String -> e
}
-> (Float -> Bool)
-> Type e Float

Similar to subsetOfFloat but you get to customize the errors.

Bool

bool : Type (Error e) 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 (Error e) 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 (Error e) 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")
{ blankError : e
, syntaxError : String -> e
, validationError : String -> e
}
-> CustomBoolOptions
-> (Bool -> Bool)
-> Type e Bool

Customize the errors and options.

import Set


type MyError
    = MyBlank
    | MySyntaxError String
    | MyValidationError String


mySubsetOfBool : (Bool -> Bool) -> Type MyError Bool
mySubsetOfBool =
    customSubsetOfBool
        { blankError = MyBlank
        , syntaxError = MySyntaxError
        , validationError = MyValidationError
        }
        { truthy = Set.fromList [ "#t" ]
        , falsy = Set.fromList [ "#f" ]
        , toString =
            \b ->
                if b then
                    "#t"


                else
                    "#f"
        , caseSensitive = True
        }


myBool : Type MyError Bool
myBool =
    mySubsetOfBool (always True)


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


myFalse : Type MyError Bool
myFalse =
    mySubsetOfBool ((==) False)
type alias CustomBoolOptions =
{ truthy : Set String
, falsy : Set String
, toString : Bool -> String
, caseSensitive : Bool
}

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 (Error e) 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 (Error e) 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 ")
{ blankError : e
, syntaxError : String -> e
, validationError : String -> e
}
-> (Char -> Bool)
-> Type e Char

Similar to subsetOfChar but you get to customize the errors.

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 (Error e) 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 ""
customSubsetOfString : (String -> e) -> (String -> Bool) -> Type e String

Similar to subsetOfString but you get to customize the validation error.

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 (Error e) 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 ")
{ blankError : e
, validationError : String -> e
}
-> (String -> Bool)
-> Type e String

Similar to subsetOfNonEmptyString but you get to customize the errors.

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 (Error e) 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
{ blankError : e
, validationError : String -> e
}
-> (String -> Bool)
-> Type e String

Similar to subsetOfNonBlankString but you get to customize the errors.

User-defined

subsetOfType : (a -> Bool) -> Type (Error e) a -> Type (Error e) 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")
customSubsetOfType : (String -> e) -> (a -> Bool) -> Type e a -> Type e a

Similar to subsetOfType but you get to customize the validation error.

{ fromString : String -> Result e a
, toString : a -> String
}
-> Type e a

Define a new field type.

type Email = Email String


email : Type (Error e) 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 (Error e) a -> Type (Error e) (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")
customOptional : (e -> Bool) -> Type e a -> Type e (Maybe a)

Similar to optional but you get to customize the blank error check.

Construct

All newly constructed fields are clean.

empty : Type e a -> Field e 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 e a -> String -> Field e a

Initialize a new field from the given type and string.

toMaybe (fromString bool "enabled") == Just True
fromValue : Type e a -> a -> Field e 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 e a -> Field e 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 e a -> Field e 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 : e -> Field e a -> Field e a
field = empty int


error = customError "Not an integer"


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


error = customError "Not an integer"


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

Query

isEmpty : Field e 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 e 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 e 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 e 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 e 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 e 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 e a -> Bool

True if the field does not have any errors.

isValid (empty int) == False


isValid (fromString int "1") == True
isInvalid : Field e 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 e a -> String
toRawString (empty int) == ""


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


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


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


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


toValidation (empty int) == V.fail blankError


toValidation (fromString int "5") == V.succeed 5
toType : Field e a -> Type e a

Recover the type information for a field.

State

type alias State e a =
{ raw : String
, processed : Validation e a
, clean : Bool
}

The internal state of a Field.

raw contains the unprocessed String input.

processed contains the result of parsing raw. Either it was successfully parsed into a type a or there were errors of type e.

clean indicates whether or not the corresponding Field is considered to be clean or dirty.

A Field is clean if it has never been changed, via one of setFromString, setFromValue, setError, setErrors, setCustomError, or setCustomErrors, after it was initially created. Otherwise, the Field is considered to be dirty.

toState : Field e a -> State e a

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 e a =
{ fromString : String -> Result e a
, fromValue : a -> Result e a
, toString : a -> String
}

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 e) Positive
fromString =
    (typeToConverters positiveInt).fromString >> Result.map Positive


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


fieldType : Type (Error e) Positive
fieldType =
    customType
        { fromString = fromString
        , toString = toString
        }
typeToConverters : Type e a -> Converters e a
toConverters : Field e a -> Converters e 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 e 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 e) a) -> String -> Result (Error e) 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 (Error String) 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" ]
customTrim : e -> (String -> Result e a) -> String -> Result e a

Similar to trim but you get to customize the blank error.

Error

type Error e

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

{ blankError : Error e
, syntaxError : String -> Error e
, validationError : String -> Error e
}

The default blank, syntax, and validation errors. It's particularly useful when customizing the Bool type with customSubsetOfBool and you don't want to customize the errors.

myBool = customSubsetOfBool defaultErrors { defaultCustomBoolOptions | caseSensitive = True } (always True)


toResult (fromString myBool "true") == Ok True


toResult (fromString bool "True") == Ok True


toResult (fromString myBool "True") == Err [ syntaxError "True" ]

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 e

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 e

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 (Error e) 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.

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 rather than a String you get to customize the type used to record any extra error details.

type Username = Username String


type UsernameError
    = TooShort { actual : Int, min : Int }
    | TooLong { actual : Int, max : Int }


username : Type (Error UsernameError) Username
username =
    customType
        { fromString =
            trim
                (\s ->
                    let
                        len =
                            String.length s
                    in
                    if len < 3 then
                        Err <| customError (TooShort { actual = len, min = 3 })
                    else if len > 25 then
                        Err <| customError (TooLong { actual = len, max = 25 })
                    else
                        Ok (Username s)
                )
        , toString =
            \(Username s) -> s
        }


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


toResult (fromString username "ab") == Err [ customError (TooShort { actual = 2, min = 3 }) ]


toResult (fromString username "abcdefghijklmnopqrstuvwxyz") == Err [ customError (TooLong { actual = 26, max = 25 }) ]

You get to design the type so that you'd have enough information available to display a user-friendly error message. See errorToString for an example.

{ onBlank : String
, onSyntaxError : String -> String
, onValidationError : String -> String
, onCustomError : e -> String
}
-> Error e
-> String

Convert your errors to user-friendly error messages.

Let's continue with the example from customError.

toErrorMessage : Error UsernameError -> String
toErrorMessage =
    errorToString
        { onBlank = "The username is required"
        , onSyntaxError = always ""
        , onValidationError = always ""
        , onCustomError =
            \e ->
                case e of
                    TooShort { actual, min } ->
                        "The username must have at least " ++ String.fromInt min ++ " characters: " ++ String.fromInt actual
                    TooLong { actual, max } ->
                        "The username must have at most " ++ String.fromInt max ++ " characters: " ++ String.fromInt actual
        }


toErrorMessage (customError <| TooShort { actual = 2, min = 3 }) == "The username must have at least 3 characters: 2"


toErrorMessage (customError <| TooLong { actual = 26, max = 25 }) == "The username must have at most 25 characters: 26"
mapErrorType : (x -> y) -> Error x -> Error y

Handle Errors

mapTypeError : (x -> y) -> Type x a -> Type y a
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


type AgeError
    = TooYoung { actual : Int, min : Int }
    | TooOld { actual : Int, max : Int }


age : Type (Error AgeError) Age
age =
    customType
        { fromString =
            (typeToConverters positiveInt).fromString
                >> Result.andThen
                    (\n ->
                        if n < 21 then
                            Err <| customError (TooYoung { actual = n, min = 21 })
                        else if n > 65 then
                            Err <| customError (TooOld { actual = n, max = 65 })
                        else
                            Ok (Age n)
                    )
        , toString =
            \(Age n) -> String.fromInt n
        }


--
-- 1. The single error type under which we will unify the two custom field errors.
--
type Errors
    = MyUsernameError (Error UsernameError)
    | MyAgeError (Error AgeError)


usernameField : Field (Error UsernameError) Username
usernameField = fromString username "ab"


ageField : Field (Error AgeError) 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 <| TooShort { actual = 2, min = 3 }), MyAgeError (customError <| TooOld { actual = 67, max = 65 }) ]
firstError : Field e a -> Maybe e
fromString string "A string"
    |> firstError
    |> (==) Nothing


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


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


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