Skip to content

Commit fae5515

Browse files
committed
traverseTaskResultA et al
1 parent 85c94d7 commit fae5515

File tree

2 files changed

+183
-5
lines changed
  • src/FsToolkit.ErrorHandling
  • tests/FsToolkit.ErrorHandling.Tests

2 files changed

+183
-5
lines changed

src/FsToolkit.ErrorHandling/Seq.fs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,73 @@ let traverseAsyncResultA f xs =
294294
/// <remarks>This function is equivalent to <see cref="traverseAsyncResultA"/> but auto-applying the 'id' function</remarks>
295295
let sequenceAsyncResultA xs = traverseAsyncResultA id xs
296296

297+
#if !FABLE_COMPILER
298+
299+
/// <summary>
300+
/// Applies a function to each element of a sequence and returns a single task result
301+
/// </summary>
302+
/// <param name="state">The initial state</param>
303+
/// <param name="f">The function to apply to each element</param>
304+
/// <param name="xs">The input sequence</param>
305+
/// <returns>A task result with the ok elements in an array or an array of all errors from the state and the original sequence</returns>
306+
let inline traverseTaskResultA'
307+
(state: TaskResult<'okOutput seq, 'error seq>)
308+
([<InlineIfLambda>] f: 'okInput -> TaskResult<'okOutput, 'error>)
309+
(xs: 'okInput seq)
310+
: TaskResult<'okOutput[], 'error[]> =
311+
if isNull xs then
312+
nullArg (nameof xs)
313+
314+
task {
315+
match! state with
316+
| Error failuresToDate ->
317+
let mutable errs = ArrayCollector()
318+
errs.AddMany failuresToDate
319+
320+
for x in xs do
321+
match! f x with
322+
| Ok _ -> () // as the initial state was failure, oks are irrelevant
323+
| Error e -> errs.Add e
324+
325+
return Error(errs.Close())
326+
| Ok initialSuccesses ->
327+
328+
let mutable oks = ArrayCollector()
329+
oks.AddMany initialSuccesses
330+
let mutable ok = true
331+
let mutable errs = ArrayCollector()
332+
333+
for x in xs do
334+
match! f x with
335+
| Ok r when ok -> oks.Add r
336+
| Ok _ -> () // no point saving results we won't use given the end result will be Error
337+
| Error e ->
338+
errs.Add e
339+
ok <- false
340+
341+
return if ok then Ok(oks.Close()) else Error(errs.Close())
342+
}
343+
344+
/// <summary>
345+
/// Applies a function to each element of a sequence and returns a single task result
346+
/// </summary>
347+
/// <param name="f">The function to apply to each element</param>
348+
/// <param name="xs">The input sequence</param>
349+
/// <returns>A task result with the ok elements in an array or an array of all errors occuring in the sequence</returns>
350+
/// <remarks>This function is equivalent to <see cref="traverseTaskResultA'"/> but applying an initial state of 'Seq.empty'</remarks>
351+
let traverseTaskResultA f xs =
352+
traverseTaskResultA' (TaskResult.ok Seq.empty) f xs
353+
354+
/// <summary>
355+
/// Converts a sequence of task results into a single task result
356+
/// </summary>
357+
/// <param name="xs">The input sequence</param>
358+
/// <returns>A task result with all ok elements in an array or an error result with an array of all errors occuring in the sequence</returns>
359+
/// <remarks>This function is equivalent to <see cref="traverseTaskResultA"/> but auto-applying the 'id' function</remarks>
360+
let sequenceTaskResultA xs = traverseTaskResultA id xs
361+
362+
#endif
363+
297364
/// <summary>
298365
/// Applies a function to each element of a sequence and returns a single option
299366
/// </summary>

tests/FsToolkit.ErrorHandling.Tests/Seq.fs

Lines changed: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,20 @@ let notifyFailure (PostId _) (UserId uId) =
452452
return Ok()
453453
}
454454

455-
let traverseAsyncResultATests =
455+
let traverseTaskResultATests =
456+
let notifyNewPostSuccess (PostId post) (UserId user) = TaskResult.ok (post, user)
457+
458+
let notifyNewPostFailure (PostId _) (UserId uId) = TaskResult.error $"error: %O{uId}"
459+
460+
let notifyFailure (PostId _) (UserId uId) =
461+
if
462+
uId = userId1
463+
|| uId = userId3
464+
then
465+
TaskResult.error $"error: %O{uId}"
466+
else
467+
TaskResult.ok ()
468+
456469
let userIds =
457470
seq {
458471
userId1
@@ -462,15 +475,17 @@ let traverseAsyncResultATests =
462475
}
463476
|> Seq.map UserId
464477

465-
testList "Seq.traverseAsyncResultA Tests" [
466-
testCaseAsync "traverseAsyncResultA with a sequence of valid data"
478+
testList "Seq.traverseTaskResultA Tests" [
479+
testCaseAsync "traverseTaskResultA with a sequence of valid data"
467480
<| async {
468481
let expected =
469482
userIds
470483
|> Seq.map (fun (UserId user) -> (newPostId, user))
471484
|> Seq.toArray
472485

473-
let! actual = Seq.traverseAsyncResultA (notifyNewPostSuccess (PostId newPostId)) userIds
486+
let! actual =
487+
Seq.traverseTaskResultA (notifyNewPostSuccess (PostId newPostId)) userIds
488+
|> Async.AwaitTask
474489

475490
let actual = Expect.wantOk actual "Expected result to be Ok"
476491

@@ -484,7 +499,9 @@ let traverseAsyncResultATests =
484499
sprintf "error: %s" (userId3.ToString())
485500
|]
486501

487-
let! actual = Seq.traverseAsyncResultA (notifyFailure (PostId newPostId)) userIds
502+
let! actual =
503+
Seq.traverseTaskResultA (notifyFailure (PostId newPostId)) userIds
504+
|> Async.AwaitTask
488505

489506
let actual = Expect.wantError actual "Expected result to be Error"
490507

@@ -533,6 +550,46 @@ let sequenceAsyncResultMTests =
533550

534551
#if !FABLE_COMPILER
535552

553+
let traverseAsyncResultATests =
554+
let userIds =
555+
seq {
556+
userId1
557+
userId2
558+
userId3
559+
userId4
560+
}
561+
|> Seq.map UserId
562+
563+
testList "Seq.traverseAsyncResultA Tests" [
564+
testCaseAsync "traverseAsyncResultA with a sequence of valid data"
565+
<| async {
566+
let expected =
567+
userIds
568+
|> Seq.map (fun (UserId user) -> (newPostId, user))
569+
|> Seq.toArray
570+
571+
let! actual = Seq.traverseAsyncResultA (notifyNewPostSuccess (PostId newPostId)) userIds
572+
573+
let actual = Expect.wantOk actual "Expected result to be Ok"
574+
575+
Expect.equal actual expected "Should have a sequence of valid data"
576+
}
577+
578+
testCaseAsync "traverseResultA with few invalid data"
579+
<| async {
580+
let expected = [|
581+
sprintf "error: %s" (userId1.ToString())
582+
sprintf "error: %s" (userId3.ToString())
583+
|]
584+
585+
let! actual = Seq.traverseAsyncResultA (notifyFailure (PostId newPostId)) userIds
586+
587+
let actual = Expect.wantError actual "Expected result to be Error"
588+
589+
Expect.equal actual expected "Should have a sequence of errors"
590+
}
591+
]
592+
536593
let sequenceTaskResultMTests =
537594
let notifyNewPostSuccess (PostId post) (UserId user) = TaskResult.ok (post, user)
538595

@@ -672,6 +729,58 @@ let sequenceAsyncResultATests =
672729
]
673730

674731
#if !FABLE_COMPILER
732+
let sequenceTaskResultATests =
733+
let notifyNewPostSuccess (PostId post) (UserId user) = TaskResult.ok (post, user)
734+
let notifyFailure (PostId _) (UserId uId) =
735+
if
736+
uId = userId1
737+
|| uId = userId3
738+
then
739+
TaskResult.error $"error: %O{uId}"
740+
else
741+
TaskResult.ok ()
742+
743+
let userIds =
744+
seq {
745+
userId1
746+
userId2
747+
userId3
748+
userId4
749+
}
750+
|> Seq.map UserId
751+
752+
testList "Seq.sequenceTaskResultA Tests" [
753+
testCaseAsync "sequenceTaskResultA with a sequence of valid data"
754+
<| async {
755+
let expected =
756+
userIds
757+
|> Seq.map (fun (UserId user) -> (newPostId, user))
758+
|> Seq.toArray
759+
760+
let actual =
761+
Seq.map (notifyNewPostSuccess (PostId newPostId)) userIds
762+
|> Seq.sequenceTaskResultA
763+
764+
do! Expect.hasTaskOkValue expected actual |> Async.AwaitTask
765+
}
766+
767+
testCaseAsync "sequenceTaskResultA with few invalid data"
768+
<| async {
769+
let expected = [|
770+
sprintf "error: %s" (userId1.ToString())
771+
sprintf "error: %s" (userId3.ToString())
772+
|]
773+
774+
let! actual =
775+
Seq.map (notifyFailure (PostId newPostId)) userIds
776+
|> Seq.sequenceTaskResultA
777+
|> Async.AwaitTask
778+
779+
let actual = Expect.wantError actual "Expected result to be Error"
780+
Expect.equal actual expected "Should have a sequence of errors"
781+
}
782+
]
783+
675784
let traverseVOptionMTests =
676785
testList "Seq.traverseVOptionM Tests" [
677786
let tryTweetVOption x =
@@ -797,11 +906,13 @@ let allTests =
797906
traverseAsyncResultATests
798907
sequenceAsyncResultMTests
799908
#if !FABLE_COMPILER
909+
traverseTaskResultATests
800910
sequenceTaskResultMTests
801911
#endif
802912
sequenceAsyncOptionMTests
803913
sequenceAsyncResultATests
804914
#if !FABLE_COMPILER
915+
sequenceTaskResultATests
805916
traverseVOptionMTests
806917
sequenceVOptionMTests
807918
#endif

0 commit comments

Comments
 (0)