Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add some extensions methods for validation #518

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
23 changes: 23 additions & 0 deletions src/FSharpPlus/Data/Validation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,29 @@ module Validation =
List.iter (function Success e -> coll1.Add e | Failure e -> coll2.Add e) source
coll1.Close (), coll2.Close ()
#endif

[<AutoOpen>]
module ComputationExpression =
type ValidationBuilder() =
member _.Bind(x, f) =
match x with
| Failure e -> Failure e
| Success a -> f a

member _.MergeSources(left : Validation<list<string>, _>, right : Validation<list<string>, _>) =
match left, right with
| Success l, Success r -> Success (l, r)
| Failure l, Success _ -> Failure l
| Success _, Failure r -> Failure r
| Failure r, Failure l -> List.append r l |> Failure

member _.Return(x) =
Success x

member _.ReturnFrom(x) =
x

let validator = ValidationBuilder()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this is just a special case of the applicative computation expression, closed to Validation<list<string>, 'T>.

I mean, right now you can just define it like: let validator<'T> = applicative<Validation<list<string>, 'T>> and you'll have the same methods and more available.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the bind is not part of the applicative CE, but Validation is not a monad, as it doesn't obey monad rules. However in order to get the short-circuit behavior you can use let validator<'T> = monad'<Result<'T, List<string>>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe an alias then?



type Validation<'err,'a> with
Expand Down
80 changes: 80 additions & 0 deletions src/FSharpPlus/Extensions/Validation.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
namespace FSharpPlus

[<RequireQualifiedAccess>]
module Validations =
rodriguestiago0 marked this conversation as resolved.
Show resolved Hide resolved
open System
open FSharpPlus.Data

let inline validate errorMessage f v =
if f v then
Success v
else
Failure [ errorMessage ]

let inline requireString propName =
let errorMessage =
sprintf "%s cannot be null, empty or whitespace." propName
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my experience using string as errors doesn't scale well, unless they are at the boundaries with the user.
But otherwise, validations errors should be represented with a custom type, normally a DU, which should be directly related to the business model of validation errors.

Having said that, we could add an additional parameter which is a function that creates the error, so the user is free to supply whatever structure he uses.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rodriguestiago0

  1. Error strings must be declared as public static functions to be available for plain reuse (for example in unit tests)
  2. We have a case right now when we need to delay error string creation till it is required:
    a. Output to log as is
    b. Output to API response with camel case field name modification

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know to let the user define the Error to be added. It can be a string, int or whatever the developer wants.


validate errorMessage (String.IsNullOrWhiteSpace >> not)

let inline requireGreaterThan propName min =
let errorMessage =
sprintf "%s have to be greater or equal to '%d'." propName min

validate errorMessage (flip (>) min)

let inline requireGreaterOrEqualThan propName min =
let errorMessage =
sprintf "%s have to be greater or equal to '%d'." propName min

validate errorMessage (flip (>=) min)

let inline requireEmail propName =
let errorMessage =
sprintf "%s is not a valid email address." propName

let check (v: string) =
try
let _ = Net.Mail.MailAddress(v)
true
with
| ex -> false

validate errorMessage check

let inline requireGuid propName =
validate (sprintf "%s is required" propName) (fun v -> v <> Guid.Empty)

let inline requireObject propName =
let check value = box value <> null
validate (sprintf "%s is required" propName) check

let inline requireWhenSome value checkWhenSome =
match value with
| Some v -> checkWhenSome v |> Validation.map Some
| _ -> Success None

let inline requireArrayValues values check =
let validated : Validation<_,_> [] =
values
|> Array.map check
validated
|> sequence
|> Validation.map Seq.toArray

let inline requireListValues values check =
let validated : List<Validation<_,_>> =
values
|> List.map check
validated
|> sequence
|> Validation.map Seq.toArray

let inline requireAtLeastOne propName =
let check values =
Seq.isEmpty values |> not

let errorMessage =
sprintf "%s should have at least one element'." propName

validate errorMessage check
1 change: 1 addition & 0 deletions src/FSharpPlus/FSharpPlus.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
<Compile Include="Data/Coproduct.fs" />
<Compile Include="Extensions/Observable.fs" />
<Compile Include="Extensions/AsyncEnumerable.fs" />
<Compile Include="Extensions/Validation.fs" />
<Compile Include="Memoization.fs" />
<Compile Include="Parsing.fs" />
</ItemGroup>
Expand Down
70 changes: 68 additions & 2 deletions tests/FSharpPlus.Tests/Validations.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,27 @@ module Validation =
open FSharpPlus.Data
open Validation
open FSharpPlus.Tests.Helpers


let private isSuccess =
function
| Success _ -> true
| Failure _ -> false

let private isFailure =
function
| Success _ -> false
| Failure _ -> true
rodriguestiago0 marked this conversation as resolved.
Show resolved Hide resolved

let private getSuccess =
function
| Success s -> s
| Failure _ -> failwith "It's a failure"
rodriguestiago0 marked this conversation as resolved.
Show resolved Hide resolved

let private getFailure =
function
| Success _ -> failwith "It's a Success"
| Failure f -> f

let fsCheck s x = Check.One({Config.QuickThrowOnFailure with Name = s}, x)
module FunctorP =
[<Test>]
Expand Down Expand Up @@ -337,4 +357,50 @@ module Validation =
let v: Validation<string Async, int Async> = Success (async {return 42})
let r = Validation.bisequence v
let subject = Async.RunSynchronously r
areStEqual subject (Success 42)
areStEqual subject (Success 42)

[<Test>]
[<TestCase("", false)>]
[<TestCase(" ", false)>]
[<TestCase(null, false)>]
[<TestCase("NotEmpty", true)>]
let testValidateRequireString (str, success) =

let r = Validations.requireString "Str" str
areStEqual (isSuccess r) success

if not success then
let failure = getFailure r
areStEqual failure.Length 1
else
()

[<Test>]
[<TestCase(1, 0, true)>]
[<TestCase(0, 0, false)>]
[<TestCase(-1, 0, false)>]
let testValidateRequireGreaterThan (value, limit, success) =

let r = Validations.requireGreaterThan "Int" limit value
areStEqual (isSuccess r) success

if not success then
let failure = getFailure r
areStEqual failure.Length 1
else
()

[<Test>]
[<TestCase(1, 0, true)>]
[<TestCase(0, 0, true)>]
[<TestCase(-1, 0, false)>]
let testValidateRequireGreaterOrEqualThan (value, limit, success) =

let r = Validations.requireGreaterOrEqualThan "Int" limit value
areStEqual (isSuccess r) success

if not success then
let failure = getFailure r
areStEqual failure.Length 1
else
()