From c6942a33a183646da6bfdd10e556bfc517bc4221 Mon Sep 17 00:00:00 2001 From: "Reuben J. Sonnenberg" Date: Mon, 9 Dec 2024 15:48:37 -0900 Subject: [PATCH 1/3] Remove internal scoping and add Async extension methods for validation contexts --- FSharp.Data.Validation.sln | 14 ++++ .../FSharp.Data.Validation.Async.fsproj | 16 +++++ src/FSharp.Data.Validation.Async/VCtx.fs | 71 +++++++++++++++++++ src/FSharp.Data.Validation/Utilities.fs | 2 +- src/FSharp.Data.Validation/VCtx.fs | 1 - 5 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 src/FSharp.Data.Validation.Async/FSharp.Data.Validation.Async.fsproj create mode 100644 src/FSharp.Data.Validation.Async/VCtx.fs diff --git a/FSharp.Data.Validation.sln b/FSharp.Data.Validation.sln index 8124946..d2b2310 100644 --- a/FSharp.Data.Validation.sln +++ b/FSharp.Data.Validation.sln @@ -13,6 +13,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.Validation.Test EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.Validation.Giraffe", "src\FSharp.Data.Validation.Giraffe\FSharp.Data.Validation.Giraffe.fsproj", "{99EA41F7-1E77-4DD2-9D2E-9F7BE20441FB}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Data.Validation.Async", "src\FSharp.Data.Validation.Async\FSharp.Data.Validation.Async.fsproj", "{CF59CFDC-CE24-4F02-A454-2FE0CC1A9FA9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -83,6 +85,18 @@ Global {99EA41F7-1E77-4DD2-9D2E-9F7BE20441FB}.Release|x64.Build.0 = Release|Any CPU {99EA41F7-1E77-4DD2-9D2E-9F7BE20441FB}.Release|x86.ActiveCfg = Release|Any CPU {99EA41F7-1E77-4DD2-9D2E-9F7BE20441FB}.Release|x86.Build.0 = Release|Any CPU + {CF59CFDC-CE24-4F02-A454-2FE0CC1A9FA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF59CFDC-CE24-4F02-A454-2FE0CC1A9FA9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF59CFDC-CE24-4F02-A454-2FE0CC1A9FA9}.Debug|x64.ActiveCfg = Debug|Any CPU + {CF59CFDC-CE24-4F02-A454-2FE0CC1A9FA9}.Debug|x64.Build.0 = Debug|Any CPU + {CF59CFDC-CE24-4F02-A454-2FE0CC1A9FA9}.Debug|x86.ActiveCfg = Debug|Any CPU + {CF59CFDC-CE24-4F02-A454-2FE0CC1A9FA9}.Debug|x86.Build.0 = Debug|Any CPU + {CF59CFDC-CE24-4F02-A454-2FE0CC1A9FA9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF59CFDC-CE24-4F02-A454-2FE0CC1A9FA9}.Release|Any CPU.Build.0 = Release|Any CPU + {CF59CFDC-CE24-4F02-A454-2FE0CC1A9FA9}.Release|x64.ActiveCfg = Release|Any CPU + {CF59CFDC-CE24-4F02-A454-2FE0CC1A9FA9}.Release|x64.Build.0 = Release|Any CPU + {CF59CFDC-CE24-4F02-A454-2FE0CC1A9FA9}.Release|x86.ActiveCfg = Release|Any CPU + {CF59CFDC-CE24-4F02-A454-2FE0CC1A9FA9}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/FSharp.Data.Validation.Async/FSharp.Data.Validation.Async.fsproj b/src/FSharp.Data.Validation.Async/FSharp.Data.Validation.Async.fsproj new file mode 100644 index 0000000..76a7af4 --- /dev/null +++ b/src/FSharp.Data.Validation.Async/FSharp.Data.Validation.Async.fsproj @@ -0,0 +1,16 @@ + + + + net6.0 + true + + + + + + + + + + + diff --git a/src/FSharp.Data.Validation.Async/VCtx.fs b/src/FSharp.Data.Validation.Async/VCtx.fs new file mode 100644 index 0000000..57b4714 --- /dev/null +++ b/src/FSharp.Data.Validation.Async/VCtx.fs @@ -0,0 +1,71 @@ +namespace FSharp.Data.Validation + +[] +module VCtx = + /// + /// Binds a function that returns an asynchronous computation to a validation context. + /// + /// + /// This function takes a function fn that transforms a value of type 'A into an + /// asynchronous computation of type Async<VCtx<'F, 'B>> and a validation context c + /// of type VCtx<'F, 'A>. It returns an asynchronous computation of type Async<VCtx<'F, 'B>>. + /// + /// The function handles the following cases: + /// + /// + /// ValidCtx a: Applies the function fn to a and returns the result. + /// + /// + /// RefutedCtx (gfs, lfs): Returns the same RefutedCtx without applying the function. + /// + /// + /// DisputedCtx (gfs, lfs, a): Applies the function fn to a and merges the results accordingly. + /// + /// + /// + /// A function that takes a value of type 'A and returns an asynchronous computation of type Async<VCtx<'F, 'B>>. + /// A validation context of type VCtx<'F, 'A>. + /// An asynchronous computation of type Async<VCtx<'F, 'B>>. + let bindToAsync (fn:'A -> Async>) (c: VCtx<'F, 'A>): Async> = + async { + match c with + | ValidCtx a -> return! fn a + | RefutedCtx (gfs,lfs) -> return RefutedCtx (gfs,lfs) + | DisputedCtx (gfs,lfs,a) -> + let! b = fn a + match b with + | ValidCtx b -> return DisputedCtx (gfs,lfs,b) + | DisputedCtx (gfs',lfs',b) -> return DisputedCtx (gfs @ gfs', Utilities.mergeFailures lfs lfs', b) + | RefutedCtx (gfs',lfs') -> return RefutedCtx (gfs @ gfs', Utilities.mergeFailures lfs lfs') + } + + /// + /// Binds a function that returns a validation context to an asynchronous computation. + /// + /// + /// This function takes a function fn that transforms a value of type 'A into a validation context + /// of type VCtx<'F, 'B> and an asynchronous computation c of type Async<VCtx<'F, 'A>>. + /// It returns an asynchronous computation of type Async<VCtx<'F, 'B>>. + /// + /// A function that takes a value of type 'A and returns a validation context of type VCtx<'F, 'B>. + /// An asynchronous computation of type Async<VCtx<'F, 'A>>. + /// An asynchronous computation of type Async<VCtx<'F, 'B>>. + let bindAsync (fn:'A -> Async>) (c: Async>): Async> = + async { + let! c' = c + return! bindToAsync fn c' + } + + /// + /// Binds a function that returns a validation context to a validation context. + /// + /// + /// This function takes a function fn that transforms a value of type 'A into a validation context + /// of type VCtx<'F, 'B> and a validation context c of type VCtx<'F, 'A>. + /// It returns a validation context of type VCtx<'F, 'B>. + /// + /// A function that takes a value of type 'A and returns a validation context of type VCtx<'F, 'B>. + /// A validation context of type VCtx<'F, 'A>. + /// A validation context of type VCtx<'F, 'B>. + let bindFromAsync (fn:'A -> VCtx<'F, 'B>) (c: Async>): Async> = + bindAsync (fn >> async.Return) c diff --git a/src/FSharp.Data.Validation/Utilities.fs b/src/FSharp.Data.Validation/Utilities.fs index edcd2db..d1e6dac 100644 --- a/src/FSharp.Data.Validation/Utilities.fs +++ b/src/FSharp.Data.Validation/Utilities.fs @@ -1,4 +1,4 @@ -module internal FSharp.Data.Validation.Utilities +module FSharp.Data.Validation.Utilities // Given a sequence of options, return list of Some let catOptions l = Seq.choose id l diff --git a/src/FSharp.Data.Validation/VCtx.fs b/src/FSharp.Data.Validation/VCtx.fs index cb9a90c..8e89e1f 100644 --- a/src/FSharp.Data.Validation/VCtx.fs +++ b/src/FSharp.Data.Validation/VCtx.fs @@ -6,7 +6,6 @@ open System.Linq.Expressions open FSharpPlus.Data type VCtx<'F, 'A> = - internal | ValidCtx of 'A | DisputedCtx of 'F list * FailureMap<'F> * 'A | RefutedCtx of 'F list * FailureMap<'F> From 9d37323e40501cb50807d5fd7fa802b53874aafb Mon Sep 17 00:00:00 2001 From: "Reuben J. Sonnenberg" Date: Mon, 9 Dec 2024 16:31:20 -0900 Subject: [PATCH 2/3] Add asynchronous validation section to the README --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/README.md b/README.md index 555bbce..f575db9 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ - [Validating Nested Types](#validating-nested-types) - [Validating Collections](#validating-collections) - [Serializing The Proof Type](#serializing-the-proof-type) + - [Validating Async Data](#validating-async-data) - [Validation Operations](#validation-operations) - [`refute*` Operations](#refute-operations) - [`refute`](#refute) @@ -1065,6 +1066,52 @@ Here is an example of what it might look like. } ``` +### Validating Async Data + +What if we need to validate data that is retrieved asynchronously? +There are three functions available in the `FSharp.Data.Validation.Async` package that can help with this: + +- `bindToAsync` +- `bindAsync` +- `bindFromAsync` + +The `bindToAsync` function is used to bind a value to an asynchronous computation. +The value is passed to the computation and the result is returned. + +The `bindAsync` function is used to bind an asynchronous computation to a value. +The computation is executed and the result is passed to the function. + +The `bindFromAsync` function is used to bind an asynchronous computation to another asynchronous computation. +The first computation is executed and the result is passed to the second computation. + +Let's say we have a function that retrieves a user's data from a database. +We want to validate the data before we use it. +We can use the `bindToAsync` function to bind the data to a validation computation. + +```fsharp +module Example + +open FSharp.Data.Validation +open FSharp.Data.Validation.Async + +let getUserData (id:int): Async = + // get user data from database + +let validateUserData (data:UserData): Proof = + validation { + withValue data + // validate data + qed + } |> fromVCtx + +let getUserDataAndValidate (id:int): Async> = + getUserData id |> bindToAsync validateUserData +``` + +The `getUserDataAndValidate` function retrieves the user data and validates it. +The `bindToAsync` function is used to bind the data to the validation computation. +The result is an asynchronous computation that returns the validated data. + ## Validation Operations ### `refute*` Operations From 22742c9fb7657b4300d4c51fe5aefe4018a236fb Mon Sep 17 00:00:00 2001 From: "Reuben J. Sonnenberg" Date: Mon, 9 Dec 2024 16:36:38 -0900 Subject: [PATCH 3/3] Add FSharp.Data.Validation.Async to release workflow --- .github/workflows/release.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d83d05d..4b2b874 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ jobs: build: runs-on: ubuntu-latest - + steps: - name: Checkout uses: actions/checkout@v2 @@ -18,7 +18,7 @@ jobs: - name: Set VERSION variable from tag run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV - + - name: Setup .NET uses: actions/setup-dotnet@v1 with: @@ -37,7 +37,9 @@ jobs: run: dotnet pack --configuration Release /p:NuGetVersion=${VERSION} /p:PackageVersion=${VERSION} --include-symbols --output . - name: Push - run: dotnet nuget push FSharp.Data.Validation.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${MTA_NUGET_KEY} --symbol-source https://symbols.nuget.org/download/symbols --symbol-api-key ${MTA_NUGET_KEY} + run: | + dotnet nuget push FSharp.Data.Validation.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${MTA_NUGET_KEY} --symbol-source https://symbols.nuget.org/download/symbols --symbol-api-key ${MTA_NUGET_KEY} + dotnet nuget push FSharp.Data.Validation.Async.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${MTA_NUGET_KEY} --symbol-source https://symbols.nuget.org/download/symbols --symbol-api-key ${MTA_NUGET_KEY} env: MTA_NUGET_KEY: ${{ secrets.MTA_NUGET_KEY }} \ No newline at end of file