Skip to content

Commit

Permalink
Merge pull request #24 from mta-solutions/additional-vctx-functions
Browse files Browse the repository at this point in the history
Add additional VCtx functions
  • Loading branch information
RJSonnenberg authored Dec 13, 2024
2 parents fb8ffa4 + f5eb5b2 commit 1f65c9a
Show file tree
Hide file tree
Showing 11 changed files with 418 additions and 46 deletions.
14 changes: 14 additions & 0 deletions FSharp.Data.Validation.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Data.Validation.Gira
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
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Data.Validation.Async.Tests", "tests\FSharp.Data.Validation.Async.Tests\FSharp.Data.Validation.Async.Tests.fsproj", "{0B92DFE3-181E-4713-B03F-B747D8D38187}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -97,6 +99,18 @@ Global
{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
{0B92DFE3-181E-4713-B03F-B747D8D38187}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0B92DFE3-181E-4713-B03F-B747D8D38187}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0B92DFE3-181E-4713-B03F-B747D8D38187}.Debug|x64.ActiveCfg = Debug|Any CPU
{0B92DFE3-181E-4713-B03F-B747D8D38187}.Debug|x64.Build.0 = Debug|Any CPU
{0B92DFE3-181E-4713-B03F-B747D8D38187}.Debug|x86.ActiveCfg = Debug|Any CPU
{0B92DFE3-181E-4713-B03F-B747D8D38187}.Debug|x86.Build.0 = Debug|Any CPU
{0B92DFE3-181E-4713-B03F-B747D8D38187}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0B92DFE3-181E-4713-B03F-B747D8D38187}.Release|Any CPU.Build.0 = Release|Any CPU
{0B92DFE3-181E-4713-B03F-B747D8D38187}.Release|x64.ActiveCfg = Release|Any CPU
{0B92DFE3-181E-4713-B03F-B747D8D38187}.Release|x64.Build.0 = Release|Any CPU
{0B92DFE3-181E-4713-B03F-B747D8D38187}.Release|x86.ActiveCfg = Release|Any CPU
{0B92DFE3-181E-4713-B03F-B747D8D38187}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
17 changes: 4 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1069,21 +1069,10 @@ 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.
There are multiple functions available in the `FSharp.Data.Validation.Async` package that can help with this.
For example, 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.
Expand Down Expand Up @@ -1112,6 +1101,8 @@ 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.

See the `FSharp.Data.Validation.Async` documentation for more information on the available functions.

## Validation Operations

### `refute*` Operations
Expand Down
16 changes: 16 additions & 0 deletions src/FSharp.Data.Validation.Async/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# FSharp.Data.Validation.Async

## Description

This library provides a small set of functions that extend the `FSharp.Data.Validation` library to work with asynchronous workflows.

## Functions

- `bindToAsync: ('A -> Async<VCtx<'F, 'B>>) -> VCtx<'F, 'A> -> Async<VCtx<'F, 'B>>`
- `bindAsync: ('A -> Async<VCtx<'F, 'B>>) -> Async<VCtx<'F, 'A>> -> Async<VCtx<'F, 'B>>`
- `bindFromAsync: ('A -> VCtx<'F, 'B>) -> Async<VCtx<'F, 'A>> -> Async<VCtx<'F, 'B>>`
- `combineAsync: Async<VCtx<'F, 'A>> -> Async<VCtx<'F, 'B>> -> Async<VCtx<'F, 'A * 'B>>`
- `bindAndMergeSourcesAsync: ('A -> Async<VCtx<'F, 'B>>) -> Async<VCtx<'F, 'A>> -> Async<VCtx<'F, 'A * 'B>>`
- `bindToAndMergeSourcesAsync: ('A -> Async<VCtx<'F, 'B>>) -> VCtx<'F, 'A> -> Async<VCtx<'F, 'A * 'B>>`
- `bindFromAndMergeSourcesAsync: ('A -> VCtx<'F, 'B>) -> Async<VCtx<'F, 'A>> -> Async<VCtx<'F, 'A * 'B>>`
- `mapAsync: ('A -> Async<'B>) -> VCtx<'F, 'A> -> Async<VCtx<'F, 'B>>`
133 changes: 122 additions & 11 deletions src/FSharp.Data.Validation.Async/VCtx.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ namespace FSharp.Data.Validation
[<RequireQualifiedAccess>]
module VCtx =
/// <summary>
/// Binds a function that returns an asynchronous computation to a validation context.
/// Binds a function that returns an asynchronous validation context to a validation context.
/// </summary>
/// <remarks>
/// This function takes a function <c>fn</c> that transforms a value of type <c>'A</c> into an
/// asynchronous computation of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c> and a validation context <c>c</c>
/// of type <c>VCtx&lt;'F, 'A&gt;</c>. It returns an asynchronous computation of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.
/// asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c> and a validation context <c>c</c>
/// of type <c>VCtx&lt;'F, 'A&gt;</c>. It returns an asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.
///
/// The function handles the following cases:
/// <list type="bullet">
Expand All @@ -23,9 +23,9 @@ module VCtx =
/// </item>
/// </list>
/// </remarks>
/// <param name="fn">A function that takes a value of type <c>'A</c> and returns an asynchronous computation of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.</param>
/// <param name="fn">A function that takes a value of type <c>'A</c> and returns an asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.</param>
/// <param name="c">A validation context of type <c>VCtx&lt;'F, 'A&gt;</c>.</param>
/// <returns>An asynchronous computation of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.</returns>
/// <returns>An asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.</returns>
let bindToAsync (fn:'A -> Async<VCtx<'F, 'B>>) (c: VCtx<'F, 'A>): Async<VCtx<'F, 'B>> =
async {
match c with
Expand All @@ -40,24 +40,24 @@ module VCtx =
}

/// <summary>
/// Binds a function that returns a validation context to an asynchronous computation.
/// Binds a function that returns an asynchronous validation context to an asynchronous validation context.
/// </summary>
/// <remarks>
/// This function takes a function <c>fn</c> that transforms a value of type <c>'A</c> into a validation context
/// of type <c>VCtx&lt;'F, 'B&gt;</c> and an asynchronous computation <c>c</c> of type <c>Async&lt;VCtx&lt;'F, 'A&gt;&gt;</c>.
/// It returns an asynchronous computation of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.
/// of type <c>VCtx&lt;'F, 'B&gt;</c> and an asynchronous validation context <c>c</c> of type <c>Async&lt;VCtx&lt;'F, 'A&gt;&gt;</c>.
/// It returns an asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.
/// </remarks>
/// <param name="fn">A function that takes a value of type <c>'A</c> and returns a validation context of type <c>VCtx&lt;'F, 'B&gt;</c>.</param>
/// <param name="c">An asynchronous computation of type <c>Async&lt;VCtx&lt;'F, 'A&gt;&gt;</c>.</param>
/// <returns>An asynchronous computation of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.</returns>
/// <param name="c">An asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'A&gt;&gt;</c>.</param>
/// <returns>An asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.</returns>
let bindAsync (fn:'A -> Async<VCtx<'F, 'B>>) (c: Async<VCtx<'F, 'A>>): Async<VCtx<'F, 'B>> =
async {
let! c' = c
return! bindToAsync fn c'
}

/// <summary>
/// Binds a function that returns a validation context to a validation context.
/// Binds a function that returns a validation context to an asynchronous validation context.
/// </summary>
/// <remarks>
/// This function takes a function <c>fn</c> that transforms a value of type <c>'A</c> into a validation context
Expand All @@ -69,3 +69,114 @@ module VCtx =
/// <returns>A validation context of type <c>VCtx&lt;'F, 'B&gt;</c>.</returns>
let bindFromAsync (fn:'A -> VCtx<'F, 'B>) (c: Async<VCtx<'F, 'A>>): Async<VCtx<'F, 'B>> =
bindAsync (fn >> async.Return) c

/// <summary>
/// Merge sources of two asynchronous computations of validation contexts into a single asynchronous validation context.
/// </summary>
/// <remarks>
/// This function takes two asynchronous validation contexts <c>c1</c> and <c>c2</c> of type <c>Async&lt;VCtx&lt;'F, 'A&gt;&gt;</c>
/// and returns an asynchronous computation of type <c>Async&lt;VCtx&lt;'F, 'A * 'B&gt;&gt;</c> that merges the results.
/// </remarks>
/// <param name="c1">An asynchronous computation of type <c>Async&lt;VCtx&lt;'F, 'A&gt;&gt;</c>.</param>
/// <param name="c2">An asynchronous computation of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.</param>
/// <returns>An asynchronous computation of type <c>Async&lt;VCtx&lt;'F, 'A * 'B&gt;&gt;</c>.</returns>
/// <seealso cref="VCtx.mergeSources"/>
let mergeSourcesAsync
(c1: Async<VCtx<'F,'A>>)
(c2: Async<VCtx<'F,'B>>)
: Async<VCtx<'F,'A * 'B>> =
async {
let! a = c1
let! b = c2
return VCtx.mergeSources a b
}

/// <summary>
/// Binds a function that returns an asynchronous validation context to an asynchronous validation context and merges the results.
/// </summary>
/// <remarks>
/// This function takes a function <c>fn</c> that transforms a value of type <c>'A</c> into an asynchronous computation
/// of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c> and an asynchronous computation <c>c</c> of type <c>Async&lt;VCtx&lt;'F, 'A&gt;&gt;</c>.
/// It returns an asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'A * 'B&gt;&gt;</c> that merges the results.
/// </remarks>
/// <param name="fn">A function that takes a value of type <c>'A</c> and returns an asynchronous validation computation of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.</param>
/// <param name="c">An asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'A&gt;&gt;</c>.</param>
/// <returns>An asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'A * 'B&gt;&gt;</c>.</returns>
/// <seealso cref="VCtx.bindAsync"/>
/// <seealso cref="VCtx.mergeSourcesAsync"/>
let bindAndMergeSourcesAsync
(fn: 'A -> Async<VCtx<'F,'B>>)
(c: Async<VCtx<'F,'A>>)
: Async<VCtx<'F,'A * 'B>> =
bindAsync fn c |> mergeSourcesAsync c

/// <summary>
/// Binds a function that returns an asynchronous validation context to a validation context and merges the results.
/// </summary>
/// <remarks>
/// This function takes a function <c>fn</c> that transforms a value of type <c>'A</c> into an
/// asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c> and a validation context <c>c</c>
/// of type <c>VCtx&lt;'F, 'A&gt;</c>. It returns an asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.
/// </remarks>
/// <param name="fn">A function that takes a value of type <c>'A</c> and returns an asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.</param>
/// <param name="c">A validation context of type <c>VCtx&lt;'F, 'A&gt;</c>.</param>
/// <returns>An asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.</returns>
/// <seealso cref="VCtx.bindToAsync"/>
/// <seealso cref="VCtx.mergeSources"/>
let bindToAndMergeSourcesAsync
(fn: 'A -> Async<VCtx<'F,'B>>)
(c: VCtx<'F,'A>)
: Async<VCtx<'F,'A * 'B>> =
async {
let! b = bindToAsync fn c
return VCtx.mergeSources c b
}

// bindFromAndMergeSourcesAsync: ('A -> VCtx<'F, 'B>) -> Async<VCtx<'F, 'A>> -> Async<VCtx<'F, 'A * 'B>>

/// <summary>
/// Binds a function that returns a validation context to an asynchronous validation context and merges the results.
/// </summary>
/// <remarks>
/// This function takes a function <c>fn</c> that transforms a value of type <c>'A</c> into a validation context
/// of type <c>VCtx&lt;'F, 'B&gt;</c> and an asynchronous validation context <c>c</c> of type <c>Async&lt;VCtx&lt;'F, 'A&gt;&gt;</c>.
/// It returns an asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'A * 'B&gt;&gt;</c> that merges the results.
/// </remarks>
/// <param name="fn">A function that takes a value of type <c>'A</c> and returns a validation context of type <c>VCtx&lt;'F, 'B&gt;</c>.</param>
/// <param name="c">An asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'A&gt;&gt;</c>.</param>
/// <returns>An asynchronous computation of type <c>Async&lt;VCtx&lt;'F, 'A * 'B&gt;&gt;</c>.</returns>
/// <seealso cref="VCtx.bindFromAsync"/>
/// <seealso cref="VCtx.mergeSources"/>
let bindFromAndMergeSourcesAsync
(fn: 'A -> VCtx<'F, 'B>)
(c: Async<VCtx<'F, 'A>>)
: Async<VCtx<'F, 'A * 'B>> =
let b = bindFromAsync fn c
mergeSourcesAsync c b

/// <summary>
/// Maps a function over the value of a validation context. The function returns an asynchronous computation.
/// </summary>
/// <remarks>
/// This function takes a function <c>fn</c> that transforms a value of type <c>'A</c> into an asynchronous computation
/// of type <c>Async&lt;'B&gt;</c> and an asynchronous validation context <c>c</c> of type <c>Async&lt;VCtx&lt;'F, 'A&gt;&gt;</c>.
/// It returns an asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.
/// </remarks>
/// <param name="fn">A function that takes a value of type <c>'A</c> and returns an asynchronous computation of type <c>Async&lt;'B&gt;</c>.</param>
/// <param name="c">An asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'A&gt;&gt;</c>.</param>
/// <returns>An asynchronous validation context of type <c>Async&lt;VCtx&lt;'F, 'B&gt;&gt;</c>.</returns>
let mapAsync
(fn: 'A -> Async<'B>)
(c: Async<VCtx<'F,'A>>)
: Async<VCtx<'F,'B>> =
async {
let! c' = c
match c' with
| ValidCtx a ->
let! b = fn a
return ValidCtx b
| RefutedCtx (gfs,lfs) -> return RefutedCtx (gfs,lfs)
| DisputedCtx (gfs,lfs,a) ->
let! b = fn a
return DisputedCtx (gfs,lfs, b)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
<PackagePath>\</PackagePath>
</None>
</ItemGroup>

<ItemGroup>
<PackageReference Update="FSharp.Core" Version="[6.0.6,)" />
<PackageReference Include="Giraffe" Version="[6,)" />
<PackageReference Include="System.Text.Json" Version="[6.0.11,)" />
</ItemGroup>
</Project>
31 changes: 23 additions & 8 deletions src/FSharp.Data.Validation/VCtx.fs
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,37 @@ module VCtx =
(gfs, Utilities.mergeFailures lfs <| Utilities.mergeFailures lfs3 lfs2)
| Global _a -> (gfs @ gfs', Utilities.mergeFailures lfs lfs')

type VCtxBuilder() =
member this.Bind(v:VCtx<'F, 'A>, fn:'A -> VCtx<'F, 'B>): VCtx<'F, 'B> =
VCtx.bind fn v

member this.MergeSources(v1: VCtx<'F, 'A>, v2: VCtx<'F, 'B>) =
/// <summary>
/// Merges two validation contexts. If one of the contexts is refuted, the result is refuted. If one of the contexts
/// is disputed, the result is disputed. Otherwise, the result is valid. The result is a tuple of the values of the
/// two input contexts.
/// </summary>
/// <param name="v1">The first validation context.</param>
/// <param name="v2">The second validation context.</param>
/// <returns>A validation context that combines the results of the two input validation contexts.</returns>
/// <remarks>
/// This function takes two validation contexts <c>v1</c> and <c>v2</c> and returns a tupled validation context.
/// Prioritizes refuted contexts over disputed contexts and disputed contexts over valid contexts.
/// </remarks>
let mergeSources (v1: VCtx<'F, 'A>) (v2: VCtx<'F, 'B>): VCtx<'F, 'A * 'B> =
match (v1, v2) with
| ValidCtx a, ValidCtx b -> ValidCtx (a, b)
| ValidCtx _, DisputedCtx (gfs', lfs', _) -> RefutedCtx (gfs', lfs')
| ValidCtx a, DisputedCtx (gfs', lfs', b) -> DisputedCtx (gfs', lfs', (a, b))
| ValidCtx _, RefutedCtx (gfs', lfs') -> RefutedCtx (gfs', lfs')
| DisputedCtx (gfs, lfs, _), ValidCtx _ -> RefutedCtx (gfs, lfs)
| DisputedCtx (gfs, lfs, _), DisputedCtx (gfs', lfs', _) -> RefutedCtx (gfs @ gfs', Utilities.mergeFailures lfs lfs')
| DisputedCtx (gfs, lfs, a), ValidCtx b -> DisputedCtx (gfs, lfs, (a, b))
| DisputedCtx (gfs, lfs, a), DisputedCtx (gfs', lfs', b) -> DisputedCtx (gfs @ gfs', Utilities.mergeFailures lfs lfs', (a, b))
| DisputedCtx (gfs, lfs, _), RefutedCtx (gfs', lfs') -> RefutedCtx (gfs @ gfs', Utilities.mergeFailures lfs lfs')
| RefutedCtx (gfs, lfs), ValidCtx _ -> RefutedCtx (gfs, lfs)
| RefutedCtx (gfs, lfs), DisputedCtx (gfs', lfs', _) -> RefutedCtx (gfs @ gfs', Utilities.mergeFailures lfs lfs')
| RefutedCtx (gfs, lfs), RefutedCtx (gfs', lfs') -> RefutedCtx (gfs @ gfs', Utilities.mergeFailures lfs lfs')

type VCtxBuilder() =
member this.Bind(v:VCtx<'F, 'A>, fn:'A -> VCtx<'F, 'B>): VCtx<'F, 'B> =
VCtx.bind fn v

member this.MergeSources(v1: VCtx<'F, 'A>, v2: VCtx<'F, 'B>) =
VCtx.mergeSources v1 v2

member this.For(v:VCtx<'F, 'A>, fn:'A -> VCtx<'F, 'B>): VCtx<'F, 'B> = this.Bind(v, fn)

member this.Return(a:'A): VCtx<'F, 'A> = ValidCtx a
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>

<IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<Compile Include="Tests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\FSharp.Data.Validation.Async\FSharp.Data.Validation.Async.fsproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Update="FSharp.Core" Version="8.0.100" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FsCheck" Version="2.16.6" />
<PackageReference Include="FsCheck.Xunit" Version="2.16.6" />
<PackageReference Include="FsUnit.xUnit" Version="6.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions tests/FSharp.Data.Validation.Async.Tests/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module Program = let [<EntryPoint>] main _ = 0
Loading

0 comments on commit 1f65c9a

Please sign in to comment.