The full code for this article is up on GitHub.
I like to the keep things as simple as possible and, ideally, reusable. At my previous employment I came up with a method of validation, but I was never entirely happy with the results. Some of the code repeated itself, and it was difficult to extend to arrays and array comparison. So, I went back to the drawing board building on top of the ideas that I created with the first validation library I created.
Here’s how the original API looked:
validate "Name" a.Name [nonEmptyString]
So, you needed to add the name of your property, the property, and a list of
validators you would like to validate. The list of validators would return a
Choice<string, 'a>
.
The new validation has a pretty straight forward API that is used throughout.
You can validate against an Object
, Array
, Primitive
, or Raw
value.
The Object
validation validates against each property in a Record
. If one
fails then it will continue to validate the other properties also. But if
there are multiple validators on a single property the first failure will
return but not the others. Object
has the following signature:
| Object of
value : Expr<'a> *
required : bool *
proof : ('a -> (string list option) list)
Where value
is the quotation value of the property you are testing.
required
tests if the property is null
or not if you mark it as true,
otherwise it doesn’t care if it is null
or not. proof
is the list of
provided validators. A single validator has the signature
a -> string option
. If the property is valid the validator returns None
otherwise it returns Some
with a message describing why it didn’t pass
validation.
I like this way of validating since it keeps things simple. The validators are
reusable functions which return a simple string option
. The validators are
composable functions that can be put in a list. The discriminated union
validation object is also composable.
Some things that I don’t like about validating this way. There is no compile time check to determine if you validated each field in an object. For that matter there is no runtime test for that either. It would be nice to have a least a runtime way to tell if all properties were tested, or, even better, a compile time way. But this works for now.
Here’s an example of using all the validators in with an object.
type Person =
{
Name : Name
BirthDate : DateTime
Favorites : string[]
FavoriteNumbers : int[]
}
static member Proof a =
[
prove <| Primitive (<@ a.BirthDate @>, true, [])
prove <| Object (<@ a.Name @>, true, Name.Proof)
prove <|
Array (
<@ a.Favorites @>,
true,
[arrayMinLength 1],
(fun favorite -> Primitive (<@ favorite @>, true, [stringMax 5]) ))
prove <|
Array (
<@ a.FavoriteNumbers @>,
true,
[arrayMinLength 1],
fun favorite -> Primitive (<@ favorite @>, true, [greaterThan 2])
)
prove <|
Raw (
(a.Favorites, a.FavoriteNumbers),
"Person.Favorites, Person.FavoriteNumbers",
[
fun (a, b) ->
match a, b with
| null, null | null, _ | _, null -> Some "Arrays must not be null."
| _ -> None
fun (a, b) ->
if a.Length = b.Length then
None
else
Some <| sprintf "Arrays must have same length but Person.Favorites has %i and Person.FavoriteNumbers has %i" a.Length b.Length
]
)
]
static member Validate a =
prove <| Object (<@ a @>, true, Person.Proof)
let jon = {
Name = { First = "Jon"; Last = "Nyman1" }
BirthDate = new DateTime(1947, 9, 9)
Favorites = [| "Reading"; "Red"; "Writing" |]
FavoriteNumbers = [| 1 |]
}
jon
|> validate Person.Validate
// |> ....
// OR
jon
|> validate (fun a -> prove <| Object (<@ a @>, true, Person.Proof))
where prove
is
let rec prove validation =
match validation with
| Primitive (v, required, fs) ->
match required, getValue v with
| true, None -> Some [sprintf "The value `%s` is required but was found to be `null`." v.Type.Name]
| false, None -> None
| _, Some value ->
fs
|> List.fold (fun acc f ->
match acc with
| None -> f value
| Some _ -> acc
) None
|> Option.map (fun x -> [printParameterWith " - " v + x] )
| Object (v, required, f) ->
match required, getValue v with
| true, None -> Some [sprintf "The object `%s` is required but was found to be `null`." (getOrElse "Unknown Parameter" <| getParameterName v)]
| false, None -> None
| _, Some x ->
f x
|> List.fold (
fun acc option ->
match option, acc with
| Some newError, Some error -> Some (List.append error newError)
| Some newError, None -> Some newError
| None, Some error -> Some error
| None, None -> None
) None
| Array (vs, required, proof, proveItems) ->
match required, getValue vs with
| true, None -> Some [sprintf "%s: %s" (getOrElse "Unknown Parameter" <| getParameterName vs) "This array is required."]
| false, None -> None
| _, Some xs ->
let validSelf =
proof
|> List.fold (fun acc f ->
match acc with
| None -> f xs
| Some _ -> acc
) None
match validSelf, obj.Equals(vs, null) with
| Some error, _ ->
Some [(printParameterWith ": " vs) + error]
| _, true ->
None
| None, false ->
xs
|> Array.fold (fun (i, acc) x ->
let i = i + 1
match (prove (proveItems x) ), acc with
| Some newError, Some error ->
(i, Some <| List.append error [prettyIndex i newError])
| Some newError, None -> (i, Some [prettyIndex i newError])
| None, Some error -> (i, Some error)
| None, None -> (i, None)
) (-1, None)
|> snd
|> Option.map (fun xs -> (printParameterWith ":" vs)::xs)
| Raw (a, msg, fs) ->
fs
|> List.fold (fun acc f ->
match acc with
| None -> f a
| Some _ -> acc
) None
|> Option.map (fun x -> [msg+" - "+x] )
And validate
is:
let validate f a =
match f a with
| Some xs -> Choice1Of2 <| String.concat "\n" xs
| None -> Choice2Of2 a
And Validate
is:
type Validate<'a> =
| Object of
value : Expr<'a> *
required : bool *
proof : ('a -> (string list option) list)
| Array of
value : Expr<'a[]> *
required : bool *
proof : ('a[] -> string Option) list *
proveItems : ('a -> Validate<'a>)
| Primitive of
value : Expr<'a> *
required : bool *
proof : ('a -> string Option) list
| Raw of
value : 'a *
message : string *
proof : ('a -> string Option) list
Other F# validation libraries (that I know of).