gege251/elm-validator-pipeline

Validate values and apply them to a user defined type.


License
BSD-3-Clause
Install
elm-package install gege251/elm-validator-pipeline 3.0.0

Documentation

Validator Pipeline

The Validator Pipeline is based on the idea of validator as a smart constructor.

In Elm we try to specify our types as narrow as possible, to avoid invalid states. However, even with types, we could only specify if something is an Int and we cannot know, if it is positive or negative, or if something is a String, but we don't know if it is a valid URL. That's what we use validators for.

With this library, we can mix the two worlds: using a validator to create a unique type, and making it the only possible route to build that type, so we can know for sure, that the type represents a valid value. Here's an example:

module Price exposing (create, toInt)

import Validator.Int
import Validator exposing (validate)

-- The constructor of Price is not exposed (it is an opaque type)
type Price = Price Int


create : Int -> Result (List String) Price
create value =
    validate (Validator.Int.min "Price should be a positive number." 0) value
        |> Result.map Price


toInt : Price -> Int
toInt (Price price) =
    price

Of course, most of the time, we have less trivial data types, for example records. For thoose, we can use the pipeline syntax, which might be similar to most us the folks from the json-decode-pipeline package. Values are checked, and applied one by one a function.

In the example below, we use the pipeline functions from the Validator.Named module, which collects errors into a Dict, so we can display errors for each field.

type alias Form =
    { name : String
    , email : String
    , age : String
    , password : String
    , message : String
    , approved : Bool
    }


type alias ValidForm =
    { name : String
    , email : String
    , age : Int
    , password : String
    , message : String
    }


validate : Form -> Validated String ValidForm
validate form =
    Ok ValidForm
        |> validate "name" (Validator.String.notEmpty "name is required") form.name
        |> validate "email" (Validator.String.isEmail "email is invalid") form.email
        |> validate "age" (Validator.String.isInt "age is not a number") form.age
        |> validateMany "password"
            [ Validator.String.hasLetter "password needs to have letters"
            , Validator.String.hasNumber "password needs to have numbers"
            ]
            form.password
        |> noCheck form.message
        |> checkOnly "approved" (Validator.Bool.isTrue "you need to approve") model.approved

You can run this validation in the update function, and do something with it depending on the result.

postForm : ValidForm -> Cmd Msg
postForm validForm =
    -- postForm only accepts validated forms
    ...

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Submitted ->
            let
                validated =
                    validate model.form
            in
            -- We save the validation result, so we can display errors in the view function
            ( { model | validated = validated }
            , case validated of
                Err _ ->
                    Cmd.none

                Ok validForm ->
                    postForm validForm
            )

And display the errors in the view.

view : Model -> Html Msg
view model =
    ...
    form [ onSubmit Submitted ]
        [ input [ onInput InputName, value model.name ] []
        , viewErrors "name" model.validated
        ...
        ]



viewErrors : String -> Validated String ValidForm -> Html Msg
viewErrors fieldName validated =
    case Validator.Named.getErrors "name" model.validated of
        Nothing ->
            text ""

        Just errors ->
            div [] (List.map (\error -> div [] [ text error ]) errors)

Please check out the examples folder to see how things work!