Skip to content

Instantly share code, notes, and snippets.

@krishnabhargav
Last active April 18, 2017 22:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save krishnabhargav/e06d2b96877e9110270d50b28a1a732f to your computer and use it in GitHub Desktop.
Save krishnabhargav/e06d2b96877e9110270d50b28a1a732f to your computer and use it in GitHub Desktop.
a quick validation description library in F#
type ValidationResult<'a> =
| Success of 'a
| Failure of string array //you can also make a ValidationFailure DU which is more "typesafe"
type Validate<'a> = ValidationResult<'a> -> ValidationResult<'a>
module Validators =
let bind<'a,'b> (f : 'a -> ValidationResult<'b>) =
function
| Success x ->
f x
| Failure errors -> Failure errors
let length tag min max f =
Validators.bind (fun x ->
let value : string = f x
if value.Length >= min && value.Length < max then
ValidationResult.Success x
else
let currentLength = value.Length
let error = sprintf "Field=%s.ExpectedLength=%d-%d.Current=%d" tag min max currentLength
Failure ([| error |]) //there can be better errors
)
let size tag min f =
Validators.bind( fun x ->
let value = f x |> Seq.length
if value >= min then
ValidationResult.Success x
else
let error = sprintf "Field=%s.ExpectedSize=%d.CurrentSize=%d" tag min value
Failure ([| error |]) //there can be better errors
)
let has tag key f =
Validators.bind ( fun x ->
match f x |> Map.tryFind key with
| Some _ -> ValidationResult.Success x
| None -> Failure ([| sprintf "Field=%s.MissingKey=%s" tag key |])
)
(* Mother of all operators; i guess. Can take any condition that we dont support today and use it here. *)
let predicate tag condition f =
Validators.bind (fun x ->
match condition <| f x with
| true -> Success x
| false -> Failure [| sprintf "Condition=%s failed" tag |]
)
let apply validation f =
Validators.bind (fun x ->
let results =
f x
|> Seq.map (Success >> validation)
let errors =
results
|> Seq.collect (function | ValidationResult.Failure (x) -> x | _ -> [||])
|> Seq.toArray
match errors with
| [||] -> Success x
| xs -> Failure xs
)
(* option type is expected to be present. Failure otherwise. If present; apply the validation result *)
let applyOpt validation f =
Validators.bind (fun x ->
let result =
f x |> Option.map (Success >> validation)
match result with
| None -> Failure [| "Missing required field" |]
| Some (ValidationResult.Failure f) -> Failure f
| Some (ValidationResult.Success _) -> Success x
)
(* a helper .. just need to ensure the value exists *)
let required tag f x =
match applyOpt id f x with
| Failure fs -> Failure (Array.append fs [|sprintf "Field=%s" tag|])
| Success x -> Success x
///example usage
type InputRecord =
{
title: string
description: string
bullets : string []
partNumber : string
attributes : Map<string, string>
mpq : string
id : string option
correlationId : string option
complexArray : string option array
}
with
static member Empty =
{
title = "Sample"
description = ""
bullets = [||]
partNumber = ""
attributes = Map.empty
mpq = ""
id = None
correlationId = None
complexArray = [||]
}
let validateRecord (ip: InputRecord) =
let describeSpecification =
applyOpt (length "id" 1 5 id) (fun x -> x.id)
>> required "correlationId" (fun x -> x.correlationId)
>> apply (required "nested" id) (fun x -> x.complexArray) //just a proof of concept .. if complexArray has some elements; then all has to be Some ..
>> (length "title" 5 500) (fun x -> x.title)
>> (length "description" 1 2000) (fun x -> x.description)
>> (size "bullets" 5) (fun x -> x.bullets)
>> (has "attributes" "id") (fun x -> x.attributes)
>> apply (length "bullet" 5 10 id) (fun x -> x.bullets)
>> apply (predicate "bulletIsUpper" (String.forall (System.Char.IsUpper)) id) (fun x -> x.bullets)
let validate =
Success ip |> describeSpecification
validate
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment