diff --git a/src/FsToolkit.ErrorHandling.JobResult/FsToolkit.ErrorHandling.JobResult.fsproj b/src/FsToolkit.ErrorHandling.JobResult/FsToolkit.ErrorHandling.JobResult.fsproj index f1da150b..fd8b9c47 100644 --- a/src/FsToolkit.ErrorHandling.JobResult/FsToolkit.ErrorHandling.JobResult.fsproj +++ b/src/FsToolkit.ErrorHandling.JobResult/FsToolkit.ErrorHandling.JobResult.fsproj @@ -21,8 +21,10 @@ - + + + diff --git a/src/FsToolkit.ErrorHandling.JobResult/JobOption.fs b/src/FsToolkit.ErrorHandling.JobResult/JobOption.fs new file mode 100644 index 00000000..eaddde68 --- /dev/null +++ b/src/FsToolkit.ErrorHandling.JobResult/JobOption.fs @@ -0,0 +1,26 @@ +namespace FsToolkit.ErrorHandling + +open Hopac +open Hopac.Infixes + +[] +module JobOption = + + let inline map f ar = + Job.map (Option.map f) ar + + let bind f (ar: Job<_>) = job { + let! opt = ar + let t = + match opt with + | Some x -> f x + | None -> job { return None } + return! t + } + + let retn x = + job { return Some x } + + let apply f x = + bind (fun f' -> + bind (fun x' -> retn (f' x')) x) f \ No newline at end of file diff --git a/src/FsToolkit.ErrorHandling.JobResult/JobOptionOp.fs b/src/FsToolkit.ErrorHandling.JobResult/JobOptionOp.fs new file mode 100644 index 00000000..3c6e9206 --- /dev/null +++ b/src/FsToolkit.ErrorHandling.JobResult/JobOptionOp.fs @@ -0,0 +1,10 @@ +namespace FsToolkit.ErrorHandling.Operator.JobOption + +open FsToolkit.ErrorHandling + +[] +module JobOption = + + let inline () f x = JobOption.map f x + let inline (<*>) f x = JobOption.apply f x + let inline (>>=) x f = JobOption.bind f x \ No newline at end of file diff --git a/src/FsToolkit.ErrorHandling.TaskResult/FsToolkit.ErrorHandling.TaskResult.fsproj b/src/FsToolkit.ErrorHandling.TaskResult/FsToolkit.ErrorHandling.TaskResult.fsproj index dc0bfece..9c1d20a2 100644 --- a/src/FsToolkit.ErrorHandling.TaskResult/FsToolkit.ErrorHandling.TaskResult.fsproj +++ b/src/FsToolkit.ErrorHandling.TaskResult/FsToolkit.ErrorHandling.TaskResult.fsproj @@ -24,7 +24,9 @@ + + diff --git a/src/FsToolkit.ErrorHandling.TaskResult/TaskOption.fs b/src/FsToolkit.ErrorHandling.TaskResult/TaskOption.fs new file mode 100644 index 00000000..a53f0d39 --- /dev/null +++ b/src/FsToolkit.ErrorHandling.TaskResult/TaskOption.fs @@ -0,0 +1,27 @@ +namespace FsToolkit.ErrorHandling + +open System.Threading.Tasks +open FSharp.Control.Tasks.V2.ContextInsensitive + +[] +module TaskOption = + + let inline map f ar = + Task.map (Option.map f) ar + + let bind f (ar: Task<_>) = + task { + let! opt = ar + let t = + match opt with + | Some x -> f x + | None -> task { return None } + return! t + } + + let retn x = + task { return Some x } + + let apply f x = + bind (fun f' -> + bind (fun x' -> retn (f' x')) x) f \ No newline at end of file diff --git a/src/FsToolkit.ErrorHandling.TaskResult/TaskOptionOp.fs b/src/FsToolkit.ErrorHandling.TaskResult/TaskOptionOp.fs new file mode 100644 index 00000000..0ccf846a --- /dev/null +++ b/src/FsToolkit.ErrorHandling.TaskResult/TaskOptionOp.fs @@ -0,0 +1,10 @@ +namespace FsToolkit.ErrorHandling.Operator.TaskOption + +open FsToolkit.ErrorHandling + +[] +module TaskOption = + + let inline () f x = TaskOption.map f x + let inline (<*>) f x = TaskOption.apply f x + let inline (>>=) x f = TaskOption.bind f x \ No newline at end of file diff --git a/src/FsToolkit.ErrorHandling/AsyncOption.fs b/src/FsToolkit.ErrorHandling/AsyncOption.fs new file mode 100644 index 00000000..cbbd9f25 --- /dev/null +++ b/src/FsToolkit.ErrorHandling/AsyncOption.fs @@ -0,0 +1,25 @@ +namespace FsToolkit.ErrorHandling + +open System.Threading.Tasks + +[] +module AsyncOption = + + let inline map f ar = + Async.map (Option.map f) ar + + let bind f ar = async { + let! opt = ar + let t = + match opt with + | Some x -> f x + | None -> async { return None } + return! t + } + + let retn x = + async { return Some x } + + let apply f x = + bind (fun f' -> + bind (fun x' -> retn (f' x')) x) f \ No newline at end of file diff --git a/src/FsToolkit.ErrorHandling/AsyncOptionOp.fs b/src/FsToolkit.ErrorHandling/AsyncOptionOp.fs new file mode 100644 index 00000000..53aa2489 --- /dev/null +++ b/src/FsToolkit.ErrorHandling/AsyncOptionOp.fs @@ -0,0 +1,10 @@ +namespace FsToolkit.ErrorHandling.Operator.AsyncOption + +open FsToolkit.ErrorHandling + +[] +module AsyncOption = + + let inline () f x = AsyncOption.map f x + let inline (<*>) f x = AsyncOption.apply f x + let inline (>>=) x f = AsyncOption.bind f x \ No newline at end of file diff --git a/src/FsToolkit.ErrorHandling/FsToolkit.ErrorHandling.fsproj b/src/FsToolkit.ErrorHandling/FsToolkit.ErrorHandling.fsproj index fb78bba1..c1bf2410 100644 --- a/src/FsToolkit.ErrorHandling/FsToolkit.ErrorHandling.fsproj +++ b/src/FsToolkit.ErrorHandling/FsToolkit.ErrorHandling.fsproj @@ -37,7 +37,9 @@ + + diff --git a/tests/FsToolkit.ErrorHandling.JobResult.Tests/Expect.JobOption.fs b/tests/FsToolkit.ErrorHandling.JobResult.Tests/Expect.JobOption.fs new file mode 100644 index 00000000..4801f19d --- /dev/null +++ b/tests/FsToolkit.ErrorHandling.JobResult.Tests/Expect.JobOption.fs @@ -0,0 +1,21 @@ +namespace Expects.JobOption + +module Expect = + open Expecto + open Hopac + + let hasJobValue v jobX = + let x = run jobX + if v = x then + () + else Tests.failtestf "Expected %A, was %A." v x + + + let hasJobSomeValue v jobX = + let x = run jobX + TestHelpers.Expect.hasSomeValue v x + + + let hasJobNoneValue jobX = + let x = run jobX + TestHelpers.Expect.hasNoneValue x diff --git a/tests/FsToolkit.ErrorHandling.JobResult.Tests/FsToolkit.ErrorHandling.JobResult.Tests.fsproj b/tests/FsToolkit.ErrorHandling.JobResult.Tests/FsToolkit.ErrorHandling.JobResult.Tests.fsproj index 78bb57cf..9b1485fc 100644 --- a/tests/FsToolkit.ErrorHandling.JobResult.Tests/FsToolkit.ErrorHandling.JobResult.Tests.fsproj +++ b/tests/FsToolkit.ErrorHandling.JobResult.Tests/FsToolkit.ErrorHandling.JobResult.Tests.fsproj @@ -15,9 +15,11 @@ + + diff --git a/tests/FsToolkit.ErrorHandling.JobResult.Tests/JobOption.fs b/tests/FsToolkit.ErrorHandling.JobResult.Tests/JobOption.fs new file mode 100644 index 00000000..22028581 --- /dev/null +++ b/tests/FsToolkit.ErrorHandling.JobResult.Tests/JobOption.fs @@ -0,0 +1,100 @@ +module JobOptionTests + + +open Expecto +open Expects.JobOption +open SampleDomain +open TestData +open TestHelpers +open FsToolkit.ErrorHandling +open FsToolkit.ErrorHandling.Operator.JobOption +open System +open Hopac + +let runJobSync = run +let createPostSome = createPostSome >> Job.fromAsync +let getFollowersSome = getFollowersSome >> Job.fromAsync +let allowedToPostOptional = allowedToPostOptional >> Job.fromAsync + +let mapTests = + testList "JobOption.map Tests" [ + testCase "map with Job(Some x)" <| fun _ -> + Job.singleton (Some validTweet) + |> JobOption.map remainingCharacters + |> Expect.hasJobSomeValue 267 + + testCase "map with Job(None)" <| fun _ -> + Job.singleton (None) + |> JobOption.map remainingCharacters + |> Expect.hasJobNoneValue + ] + +let bindTests = + testList "JobOption.bind tests" [ + testCase "bind with Job(Some x)" <| fun _ -> + allowedToPostOptional sampleUserId + |> JobOption.bind (fun isAllowed -> job { + if isAllowed then + return! createPostSome validCreatePostRequest + else + return None }) + |> Expect.hasJobSomeValue (PostId newPostId) + + testCase "bind with Job(None)" <| fun _ -> + allowedToPostOptional (UserId (Guid.NewGuid())) + |> JobOption.bind (fun isAllowed -> job {return Some isAllowed}) + |> Expect.hasJobNoneValue + + testCase "bind with Job(Ok x) that returns Job (None)" <| fun _ -> + allowedToPostOptional sampleUserId + |> JobOption.bind (fun _ -> job { + return None + }) + |> Expect.hasJobNoneValue + ] + +let applyTests = + testList "JobOption.apply Tests" [ + testCase "apply with Job(Some x)" <| fun _ -> + Job.singleton (Some validTweet) + |> JobOption.apply (Job.singleton (Some remainingCharacters)) + |> Expect.hasJobSomeValue (267) + + testCase "apply with Job(None)" <| fun _ -> + Job.singleton None + |> JobOption.apply (Job.singleton (Some remainingCharacters)) + |> Expect.hasJobNoneValue + ] + +let retnTests = + testList "JobOption.retn Tests" [ + testCase "retn with x" <| fun _ -> + JobOption.retn 267 + |> Expect.hasJobSomeValue (267) + ] + +let jobOptionOperatorTests = + testList "JobOption Operators Tests" [ + testCase "map & apply operators" <| fun _ -> + let getFollowersResult = getFollowersSome sampleUserId + let createPostResult = createPostSome validCreatePostRequest + newPostRequest getFollowersResult <*> createPostResult + |> Expect.hasJobSomeValue {NewPostId = PostId newPostId; UserIds = followerIds} + + testCase "bind operator" <| fun _ -> + allowedToPostOptional sampleUserId + >>= (fun isAllowed -> + if isAllowed then + createPostSome validCreatePostRequest + else + Job.singleton None) + |> Expect.hasJobSomeValue (PostId newPostId) + ] + +let allTests = testList "Job Option Tests" [ + mapTests + bindTests + applyTests + retnTests + jobOptionOperatorTests +] \ No newline at end of file diff --git a/tests/FsToolkit.ErrorHandling.TaskResult.Tests/FsToolkit.ErrorHandling.TaskResult.Tests.fsproj b/tests/FsToolkit.ErrorHandling.TaskResult.Tests/FsToolkit.ErrorHandling.TaskResult.Tests.fsproj index 3476f254..f8677278 100644 --- a/tests/FsToolkit.ErrorHandling.TaskResult.Tests/FsToolkit.ErrorHandling.TaskResult.Tests.fsproj +++ b/tests/FsToolkit.ErrorHandling.TaskResult.Tests/FsToolkit.ErrorHandling.TaskResult.Tests.fsproj @@ -19,6 +19,7 @@ + diff --git a/tests/FsToolkit.ErrorHandling.TaskResult.Tests/TaskOption.fs b/tests/FsToolkit.ErrorHandling.TaskResult.Tests/TaskOption.fs new file mode 100644 index 00000000..5e8cc33e --- /dev/null +++ b/tests/FsToolkit.ErrorHandling.TaskResult.Tests/TaskOption.fs @@ -0,0 +1,106 @@ +module TaskOptionTests + + +#if FABLE_COMPILER +open Fable.Mocha +#else +open Expecto +#endif + +open System +open System.Threading.Tasks +open FSharp.Control.Tasks.V2.ContextInsensitive +open TestData +open TestHelpers +open SampleDomain +open FsToolkit.ErrorHandling +open FsToolkit.ErrorHandling.Operator.TaskOption + +let runTaskSync (task : Task<_>) = + task.Result +let createPostSome = createPostSome >> Async.StartAsTask +let getFollowersSome = getFollowersSome >> Async.StartAsTask +let allowedToPostOptional = allowedToPostOptional >> Async.StartAsTask + +let mapTests = + testList "TaskOption.map Tests" [ + testCase"map with Task(Some x)" <| fun _ -> + Task.singleton (Some validTweet) + |> TaskOption.map remainingCharacters + |> Expect.hasTaskSomeValue 267 + + testCase "map with Task(None)" <| fun _ -> + Task.singleton (None) + |> TaskOption.map remainingCharacters + |> Expect.hasTaskNoneValue + ] + +let bindTests = + testList "TaskOption.bind tests" [ + testCase "bind with Task(Some x)" <| fun _ -> + allowedToPostOptional sampleUserId + |> TaskOption.bind (fun isAllowed -> task { + if isAllowed then + return! createPostSome validCreatePostRequest + else + return None }) + |> Expect.hasTaskSomeValue (PostId newPostId) + + testCase "bind with Task(None)" <| fun _ -> + allowedToPostOptional (UserId (Guid.NewGuid())) + |> TaskOption.bind (fun isAllowed -> task {return Some isAllowed}) + |> Expect.hasTaskNoneValue + + testCase "bind with Task(Ok x) that returns Task (None)" <| fun _ -> + allowedToPostOptional sampleUserId + |> TaskOption.bind (fun _ -> task { + return None + }) + |> Expect.hasTaskNoneValue + ] + +let applyTests = + testList "TaskOption.apply Tests" [ + testCase "apply with Task(Some x)" <| fun _ -> + Task.singleton (Some validTweet) + |> TaskOption.apply (Task.singleton (Some remainingCharacters)) + |> Expect.hasTaskSomeValue (267) + + testCase "apply with Task(None)" <| fun _ -> + Task.singleton None + |> TaskOption.apply (Task.singleton (Some remainingCharacters)) + |> Expect.hasTaskNoneValue + ] + +let retnTests = + testList "TaskOption.retn Tests" [ + testCase "retn with x" <| fun _ -> + TaskOption.retn 267 + |> Expect.hasTaskSomeValue (267) + ] + +let taskOptionOperatorTests = + testList "TaskOption Operators Tests" [ + testCase "map & apply operators" <| fun _ -> + let getFollowersResult = getFollowersSome sampleUserId + let createPostResult = createPostSome validCreatePostRequest + newPostRequest getFollowersResult <*> createPostResult + |> Expect.hasTaskSomeValue {NewPostId = PostId newPostId; UserIds = followerIds} + + testCase "bind operator" <| fun _ -> + allowedToPostOptional sampleUserId + >>= (fun isAllowed -> + if isAllowed then + createPostSome validCreatePostRequest + else + Task.singleton None) + |> Expect.hasTaskSomeValue (PostId newPostId) + ] + +let allTests = testList "Task Option Tests" [ + mapTests + bindTests + applyTests + retnTests + taskOptionOperatorTests +] \ No newline at end of file diff --git a/tests/FsToolkit.ErrorHandling.Tests/AsyncOption.fs b/tests/FsToolkit.ErrorHandling.Tests/AsyncOption.fs new file mode 100644 index 00000000..7a2cefaf --- /dev/null +++ b/tests/FsToolkit.ErrorHandling.Tests/AsyncOption.fs @@ -0,0 +1,102 @@ +module AsyncOptionTests + + +#if FABLE_COMPILER +open Fable.Mocha +#else +open Expecto +#endif + +open System +open TestData +open TestHelpers +open SampleDomain +open FsToolkit.ErrorHandling +open FsToolkit.ErrorHandling.Operator.AsyncOption + +let mapTests = + testList "AsyncOption.map Tests" [ + testCaseAsync "map with Async(Some x)" <| + (Async.singleton (Some validTweet) + |> AsyncOption.map remainingCharacters + |> Expect.hasAsyncSomeValue 267) + + testCaseAsync "map with Async(None)" <| + (Async.singleton (None) + |> AsyncOption.map remainingCharacters + |> Expect.hasAsyncNoneValue) + ] + +let bindTests = + testList "AsyncOption.bind tests" [ + testCaseAsync "bind with Async(Some x)" <| + (allowedToPostOptional sampleUserId + |> AsyncOption.bind (fun isAllowed -> async { + if isAllowed then + return! createPostSome validCreatePostRequest + else + return None }) + |> Expect.hasAsyncSomeValue (PostId newPostId)) + + testCaseAsync "bind with Async(None)" <| + (allowedToPostOptional (UserId (Guid.NewGuid())) + |> AsyncOption.bind (fun isAllowed -> async {return Some isAllowed}) + |> Expect.hasAsyncNoneValue ) + + testCaseAsync "bind with Async(Ok x) that returns Async (None)" <| + (allowedToPostOptional sampleUserId + |> AsyncOption.bind (fun _ -> async { + return None + }) + |> Expect.hasAsyncNoneValue) + ] + +let applyTests = + testList "AsyncOption.apply Tests" [ + testCaseAsync "apply with Async(Some x)" <| ( + Async.singleton (Some validTweet) + |> AsyncOption.apply (Async.singleton (Some remainingCharacters)) + |> Expect.hasAsyncSomeValue (267)) + + testCaseAsync "apply with Async(None)" <| ( + Async.singleton None + |> AsyncOption.apply (Async.singleton (Some remainingCharacters)) + |> Expect.hasAsyncNoneValue) + ] + +let retnTests = + testList "AsyncOption.retn Tests" [ + testCaseAsync "retn with x" <| ( + AsyncOption.retn 267 + |> Expect.hasAsyncSomeValue (267)) + ] + +let asyncOptionOperatorTests = + testList "AsyncOption Operators Tests" [ + testCaseAsync "map & apply operators" <| async { + let getFollowersResult = getFollowersSome sampleUserId + let createPostResult = createPostSome validCreatePostRequest + do! + newPostRequest getFollowersResult <*> createPostResult + |> Expect.hasAsyncSomeValue {NewPostId = PostId newPostId; UserIds = followerIds} + } + + testCaseAsync "bind operator" <| async { + do! + allowedToPostOptional sampleUserId + >>= (fun isAllowed -> + if isAllowed then + createPostSome validCreatePostRequest + else + Async.singleton None) + |> Expect.hasAsyncSomeValue (PostId newPostId) + } + ] + +let allTests = testList "Async Option Tests" [ + mapTests + bindTests + applyTests + retnTests + asyncOptionOperatorTests +] \ No newline at end of file diff --git a/tests/FsToolkit.ErrorHandling.Tests/Expect.fs b/tests/FsToolkit.ErrorHandling.Tests/Expect.fs index 14787c73..b234af5b 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/Expect.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/Expect.fs @@ -37,6 +37,20 @@ module Expect = | Error x -> Tests.failtestf "Expected Ok, was Error(%A)." x + let hasSomeValue v x = + match x with + | Some x when x = v -> () + | Some x -> + Tests.failtestf "Expected Some(%A), was Some(%A)." v x + | None -> + Tests.failtestf "Expected Some, was None." + + let hasNoneValue x = + match x with + | None -> () + | Some _ -> + Tests.failtestf "Expected None, was Some." + let hasAsyncValue v asyncX = async { let! x = asyncX if v = x then @@ -69,6 +83,24 @@ module Expect = let x = taskX |> Async.AwaitTask |> Async.RunSynchronously hasErrorValue v x + let hasAsyncSomeValue v asyncX = async { + let! x = asyncX + hasSomeValue v x + } + + let hasTaskSomeValue v taskX = + let x = taskX |> Async.AwaitTask |> Async.RunSynchronously + hasSomeValue v x + + let hasAsyncNoneValue asyncX = async { + let! x = asyncX + hasNoneValue x + } + + let hasTaskNoneValue taskX = + let x = taskX |> Async.AwaitTask |> Async.RunSynchronously + hasNoneValue x + let same expected actual = Expect.equal actual expected "expected and actual should be same" diff --git a/tests/FsToolkit.ErrorHandling.Tests/FsToolkit.ErrorHandling.Tests.fsproj b/tests/FsToolkit.ErrorHandling.Tests/FsToolkit.ErrorHandling.Tests.fsproj index e6ce20ab..479247f4 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/FsToolkit.ErrorHandling.Tests.fsproj +++ b/tests/FsToolkit.ErrorHandling.Tests/FsToolkit.ErrorHandling.Tests.fsproj @@ -20,6 +20,7 @@ + diff --git a/tests/FsToolkit.ErrorHandling.Tests/Main.fs b/tests/FsToolkit.ErrorHandling.Tests/Main.fs index beed3769..1227c0e4 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/Main.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/Main.fs @@ -12,6 +12,7 @@ let allTests = testList "All Tests" [ ResultOptionTests.allTests OptionTests.allTests OptionCETests.allTests + AsyncOptionTests.allTests AsyncOptionCETests.allTests ListTests.allTests AsyncResultTests.allTests diff --git a/tests/FsToolkit.ErrorHandling.Tests/SampleDomain.fs b/tests/FsToolkit.ErrorHandling.Tests/SampleDomain.fs index c394fd82..975cb147 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/SampleDomain.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/SampleDomain.fs @@ -175,6 +175,13 @@ let allowedToPost userId = async { return Error commonEx } +let allowedToPostOptional userId = async { + if (userId = sampleUserId) then + return Some true + else + return None +} + let newPostId = Guid.NewGuid() type PostId = PostId of Guid @@ -194,13 +201,19 @@ let createPostSuccess (_ : CreatePostRequest) = async { return Ok samplePostId } +let createPostSome (_ : CreatePostRequest) = async { + return Some samplePostId +} + let followerIds = [UserId (Guid.NewGuid()); UserId (Guid.NewGuid())] let getFollowersSuccess (UserId _) = async { return Ok followerIds } - +let getFollowersSome (UserId _) = async { + return Some followerIds +} let getFollowersFailure (UserId _) = async { return Error getFollowersEx