I’ve been implementing RealWorld for myself from scratch and I discovered an interesting solution to form validation when I was implementing the registration page.
Here are the bits and pieces that make up my solution to form validation in src/Page/Register.elm
:
type alias Model =
{ username : String
, email : String
, password : String
, errorMessages : List String
, isDisabled : Bool
}
type alias ValidatedFields =
{ username : Username
, email : Email
, password : Password
}
validate : Model -> V.Validation ValidatedFields
validate { username, email, password } =
V.succeed ValidatedFields
|> V.required (validateUsername username)
|> V.required (validateEmail email)
|> V.required (validatePassword password)
validateUsername : String -> V.Validation Username
validateUsername rawUsername =
case Username.fromString rawUsername of
Just username ->
V.succeed username
Nothing ->
V.fail "username can't be blank"
validateEmail : String -> V.Validation Email
validateEmail rawEmail =
case Email.fromString rawEmail of
Just email ->
V.succeed email
Nothing ->
V.fail "email can't be blank"
validatePassword : String -> V.Validation Password
validatePassword rawPassword =
case Password.fromString rawPassword of
Ok password ->
V.succeed password
Err Password.Blank ->
V.fail "password can't be blank"
Err (Password.TooShort expectedLength) ->
V.fail <|
String.concat
[ "password must be at least "
, String.fromInt expectedLength
, " "
, String.pluralize
expectedLength
{ singular = "character"
, plural = "characters"
}
, " long"
]
I’m obviously biased but I prefer my solution over the one given in elm-spa (rtfeldman).
When the form is submitted we handle the validation as follows:
SubmittedForm ->
validate model
|> V.withValidation
{ onSuccess =
\{ username, email, password } ->
( { model | errorMessages = [], isDisabled = True }
, Register.register
options.apiUrl
{ username = username
, email = email
, password = password
, onResponse = GotRegisterResponse
}
|> Cmd.map options.onChange
)
, onFailure =
\errorMessages ->
( { model | errorMessages = errorMessages }
, Cmd.none
)
}
The validation module exposes an API similar to NoRedInk/elm-json-decode-pipeline
.
module Lib.Validation exposing
( Validation
, fail
, required
, succeed
, withValidation
)
type Validation a
= Success a
| Failure (List String)
succeed : a -> Validation a
succeed =
Success
fail : String -> Validation a
fail message =
Failure [ message ]
required : Validation a -> Validation (a -> b) -> Validation b
required va vf =
case ( vf, va ) of
( Success f, Success a ) ->
Success (f a)
( Failure es, Success _ ) ->
Failure es
( Success _, Failure es ) ->
Failure es
( Failure es1, Failure es2 ) ->
Failure (es1 ++ es2)
withValidation :
{ onSuccess : a -> b
, onFailure : List String -> b
}
-> Validation a
-> b
withValidation { onSuccess, onFailure } va =
case va of
Success a ->
onSuccess a
Failure es ->
onFailure es
The overall pattern seems to be:
- Define your fields to be whatever input you’re willing to accept.
- Define a record to hold the cleaned/trimmed/validated data, see
ValidatedFields
.
- Define your validation functions for the individual fields however you like, they return
Validation a
.
- Compose your validation functions, see
validate
, using functions from the validation module, for e.g. required
. But you could implement optional
and any other function you desire.
- Use
withValidation
when it’s time to perform the validation.
I don’t know how this scales to more complicated forms but I currently like where the abstraction is heading and how it works for these simple forms in the RealWorld app.
An interesting side note
I’ve been exploring applicative style APIs and I recently came across Applicative Programming, Disjoint Unions, Semigroups and Non-breaking Error Handling by Tony Morris. The API I stumbled upon happens to be the same API, semantically speaking, that he describes in that document. According to him, “Scalaz uses this API for web form field validation. [*]”