diff --git a/src/FsToolkit.ErrorHandling/List.fs b/src/FsToolkit.ErrorHandling/List.fs index c6e69181..974cefe0 100644 --- a/src/FsToolkit.ErrorHandling/List.fs +++ b/src/FsToolkit.ErrorHandling/List.fs @@ -52,7 +52,6 @@ module List = let sequenceAsyncResultM xs = traverseAsyncResultM id xs - let rec private traverseResultA' state f xs = match xs with | [] -> @@ -115,8 +114,52 @@ module List = let sequenceValidationA xs = traverseValidationA id xs - let traverseAsyncResultA f xs = traverseAsyncResultA' (AsyncResult.retn []) f xs let sequenceAsyncResultA xs = traverseAsyncResultA id xs + + let rec private traverseOptionM' (state: Option<_>) (f: _ -> Option<_>) xs = + match xs with + | [] -> + state + |> Option.map List.rev + | x :: xs -> + let r = + option { + let! y = f x + let! ys = state + return y :: ys + } + + match r with + | Some _ -> traverseOptionM' r f xs + | None -> r + + let rec private traverseAsyncOptionM' (state: Async>) (f: _ -> Async>) xs = + match xs with + | [] -> + state + |> AsyncOption.map List.rev + | x :: xs -> + async { + let! o = + asyncOption { + let! y = f x + let! ys = state + return y :: ys + } + + match o with + | Some _ -> return! traverseAsyncOptionM' (Async.singleton o) f xs + | None -> return o + } + + let traverseOptionM f xs = traverseOptionM' (Some []) f xs + + let sequenceOptionM xs = traverseOptionM id xs + + let traverseAsyncOptionM f xs = + traverseAsyncOptionM' (AsyncOption.retn []) f xs + + let sequenceAsyncOptionM xs = traverseAsyncOptionM id xs diff --git a/tests/FsToolkit.ErrorHandling.Tests/List.fs b/tests/FsToolkit.ErrorHandling.Tests/List.fs index 8706e41d..1ecfbc59 100644 --- a/tests/FsToolkit.ErrorHandling.Tests/List.fs +++ b/tests/FsToolkit.ErrorHandling.Tests/List.fs @@ -47,6 +47,39 @@ let traverseResultMTests = "traverse the list and return the first error" ] +let traverseOptionMTests = + testList "List.traverseOptionM Tests" [ + let tryTweetOption x = + match x with + | x when String.IsNullOrEmpty x -> None + | _ -> Some x + + testCase "traverseOption with a list of valid data" + <| fun _ -> + let tweets = [ + "Hi" + "Hello" + "Hola" + ] + + let expected = Some tweets + let actual = List.traverseOptionM tryTweetOption tweets + + Expect.equal actual expected "Should have a list of valid tweets" + + testCase "traverseOption with few invalid data" + <| fun _ -> + let tweets = [ + "Hi" + "Hello" + String.Empty + ] + + let expected = None + let actual = List.traverseOptionM tryTweetOption tweets + + Expect.equal actual expected "traverse the list and return none" + ] let sequenceResultMTests = testList "List.sequenceResultM Tests" [ @@ -82,6 +115,38 @@ let sequenceResultMTests = "traverse the list and return the first error" ] +let sequenceOptionMTests = + testList "List.sequenceOptionM Tests" [ + let tryTweetOption x = + match x with + | x when String.IsNullOrEmpty x -> None + | _ -> Some x + + testCase "traverseOption with a list of valid data" + <| fun _ -> + let tweets = [ + "Hi" + "Hello" + "Hola" + ] + + let expected = Some tweets + let actual = List.sequenceOptionM (List.map tryTweetOption tweets) + + Expect.equal actual expected "Should have a list of valid tweets" + + testCase "sequenceOptionM with few invalid data" + <| fun _ -> + let tweets = [ + String.Empty + "Hello" + String.Empty + ] + + let actual = List.sequenceOptionM (List.map tryTweetOption tweets) + + Expect.equal actual None "traverse the list and return none" + ] let traverseResultATests = testList "List.traverseResultA Tests" [ @@ -285,6 +350,38 @@ let traverseAsyncResultMTests = } ] +let traverseAsyncOptionMTests = + + let userIds = [ + userId1 + userId2 + userId3 + ] + + testList "List.traverseAsyncOptionM Tests" [ + testCaseAsync "traverseAsyncOptionM with a list of valid data" + <| async { + let expected = Some userIds + let f x = async { return Some x } + let actual = List.traverseAsyncOptionM f userIds + + match expected with + | Some e -> do! Expect.hasAsyncSomeValue e actual + | None -> failwith "Error in the test case code" + } + + testCaseAsync "traverseOptionA with few invalid data" + <| async { + let expected = None + let f _ = async { return None } + let actual = List.traverseAsyncOptionM f userIds + + match expected with + | Some _ -> failwith "Error in the test case code" + | None -> do! Expect.hasAsyncNoneValue actual + } + ] + let notifyFailure (PostId _) (UserId uId) = async { if @@ -370,6 +467,43 @@ let sequenceAsyncResultMTests = } ] +let sequenceAsyncOptionMTests = + + let userIds = [ + userId1 + userId2 + userId3 + ] + + testList "List.sequenceAsyncOptionM Tests" [ + testCaseAsync "sequenceAsyncOptionM with a list of valid data" + <| async { + let expected = Some userIds + let f x = async { return Some x } + + let actual = + List.map f userIds + |> List.sequenceAsyncOptionM + + match expected with + | Some e -> do! Expect.hasAsyncSomeValue e actual + | None -> failwith "Error in the test case code" + } + + testCaseAsync "sequenceOptionA with few invalid data" + <| async { + let expected = None + let f _ = async { return None } + + let actual = + List.map f userIds + |> List.sequenceAsyncOptionM + + match expected with + | Some _ -> failwith "Error in the test case code" + | None -> do! Expect.hasAsyncNoneValue actual + } + ] let sequenceAsyncResultATests = let userIds = @@ -412,13 +546,17 @@ let sequenceAsyncResultATests = let allTests = testList "List Tests" [ traverseResultMTests + traverseOptionMTests sequenceResultMTests + sequenceOptionMTests traverseResultATests sequenceResultATests traverseValidationATests sequenceValidationATests traverseAsyncResultMTests + traverseAsyncOptionMTests traverseAsyncResultATests sequenceAsyncResultMTests + sequenceAsyncOptionMTests sequenceAsyncResultATests ]