diff --git a/README.md b/README.md index c7e5069..5d950bc 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,67 @@ my ($req, $err) = $code->($input); This allows you to describe multiple processes concisely as a single flow. Each function in the pipeline needs to return `(T, E)`. +### combine(@results) + +`combine` takes a list of Result like `((T1,E1), (T2,E2), (T3,E3))` and returns a new Result like `([T1,T2,T3], E)`. + +If all Result values are successful, it returns a new Result with all success values collected into an array reference. If any Result has an error, the function short-circuits and returns the first error encountered. + +This is useful when you need to collect the results of multiple operations that all need to succeed, similar to how `Promise.all` works in JavaScript. For example, when fetching data from multiple sources or validating multiple aspects of input data. + +Example: + +```perl +sub fetch_user { ... } # Returns Result +sub fetch_orders { ... } # Returns Result +sub fetch_settings { ... } # Returns Result + +my ($data, $err) = combine( + fetch_user($user_id), + fetch_orders($user_id), + fetch_settings($user_id) +); + +if ($err) { + # Handle error +} else { + my ($user, $orders, $settings) = @$data; + # Process all successful results +} +``` + +### combine\_with\_all\_errors(@results) + +`combine_with_all_errors` takes a list of Result like `((T1,E1), (T2,E2), (T3,E3))` and returns a new Result. + +Unlike `combine` which stops at the first error, this function collects all errors from the input Results. If all Results are successful, it returns `([T1,T2,T3], undef)`. If any Results have errors, it returns `(undef, [E1,E2,E3])` with an array reference containing all encountered errors. + +This is particularly useful for validation scenarios where you want to report all validation errors at once rather than one at a time. For example, when validating a form, you might want to show the user all fields that have errors rather than making them fix one error at a time. + +Example: + +```perl +sub validate_name { ... } # Returns Result +sub validate_email { ... } # Returns Result +sub validate_age { ... } # Returns Result + +my ($data, $errors) = combine_with_all_errors( + validate_name($form->{name}), + validate_email($form->{email}), + validate_age($form->{age}) +); + +if ($errors) { + # Show all validation errors to the user + for my $error (@$errors) { + print "Error: $error->{message}\n"; + } +} else { + my ($name, $email, $age) = @$data; + # Process valid form data +} +``` + ### unsafe\_unwrap($data, $err) `unsafe_unwrap` takes a Result and returns a T when the result is an Ok, otherwise it throws exception. diff --git a/lib/Result/Simple.pm b/lib/Result/Simple.pm index f367ffd..260f66c 100644 --- a/lib/Result/Simple.pm +++ b/lib/Result/Simple.pm @@ -10,6 +10,8 @@ use Exporter::Shiny qw( result_for chain pipeline + combine + combine_with_all_errors unsafe_unwrap unsafe_unwrap_err ); @@ -194,6 +196,49 @@ sub pipeline { return $pipelined; } +# `combine` takes a list of Result like `((T1,E1), (T2,E2), (T3,E3))` and returns a new Result like `([T1,T2,T3], E)`. +sub combine { + my @results = @_; + + if (CHECK_ENABLED) { + croak "`combine` must be called in list context" unless wantarray; + croak "`combine` arguments must be Result list" unless @_ % 2 == 0; + } + + my @values; + for (my $i = 0; $i < @results; $i += 2) { + my ($value, $error) = @results[$i, $i + 1]; + if ($error) { + return err($error); + } + push @values, $value; + } + return ok(\@values); +} + +# `combine_with_all_errors` takes a list of Result like `((T1,E1), (T2,E2), (T3,E3))` and returns a new Result like `([T1,T2,T3], [E1,E2,E3])`. +sub combine_with_all_errors { + my @results = @_; + + if (CHECK_ENABLED) { + croak "`combine_with_all_errors` must be called in list context" unless wantarray; + croak "`combine_with_all_errors` arguments must be Result list" unless @_ % 2 == 0; + } + + my @values; + my @errors; + for (my $i = 0; $i < @results; $i += 2) { + my ($value, $err) = @results[$i, $i + 1]; + if ($err) { + push @errors, $err; + } else { + push @values, $value; + } + } + return err(\@errors) if @errors; + return ok(\@values); +} + # `unsafe_nwrap` takes a Result and returns a T when the result is an Ok, otherwise it throws exception. # It should be used in tests or debugging code. sub unsafe_unwrap { @@ -412,6 +457,63 @@ Example: This allows you to describe multiple processes concisely as a single flow. Each function in the pipeline needs to return C<(T, E)>. +=head3 combine(@results) + +C takes a list of Result like C<((T1,E1), (T2,E2), (T3,E3))> and returns a new Result like C<([T1,T2,T3], E)>. + +If all Result values are successful, it returns a new Result with all success values collected into an array reference. If any Result has an error, the function short-circuits and returns the first error encountered. + +This is useful when you need to collect the results of multiple operations that all need to succeed, similar to how C works in JavaScript. For example, when fetching data from multiple sources or validating multiple aspects of input data. + +Example: + + sub fetch_user { ... } # Returns Result + sub fetch_orders { ... } # Returns Result + sub fetch_settings { ... } # Returns Result + + my ($data, $err) = combine( + fetch_user($user_id), + fetch_orders($user_id), + fetch_settings($user_id) + ); + + if ($err) { + # Handle error + } else { + my ($user, $orders, $settings) = @$data; + # Process all successful results + } + +=head3 combine_with_all_errors(@results) + +C takes a list of Result like C<((T1,E1), (T2,E2), (T3,E3))> and returns a new Result. + +Unlike C which stops at the first error, this function collects all errors from the input Results. If all Results are successful, it returns C<([T1,T2,T3], undef)>. If any Results have errors, it returns C<(undef, [E1,E2,E3])> with an array reference containing all encountered errors. + +This is particularly useful for validation scenarios where you want to report all validation errors at once rather than one at a time. For example, when validating a form, you might want to show the user all fields that have errors rather than making them fix one error at a time. + +Example: + + sub validate_name { ... } # Returns Result + sub validate_email { ... } # Returns Result + sub validate_age { ... } # Returns Result + + my ($data, $errors) = combine_with_all_errors( + validate_name($form->{name}), + validate_email($form->{email}), + validate_age($form->{age}) + ); + + if ($errors) { + # Show all validation errors to the user + for my $error (@$errors) { + print "Error: $error->{message}\n"; + } + } else { + my ($name, $email, $age) = @$data; + # Process valid form data + } + =head3 unsafe_unwrap($data, $err) C takes a Result and returns a T when the result is an Ok, otherwise it throws exception. diff --git a/t/Result-Simple.t b/t/Result-Simple.t index ee5a2bf..118ed4e 100644 --- a/t/Result-Simple.t +++ b/t/Result-Simple.t @@ -14,7 +14,7 @@ BEGIN { # $ENV{RESULT_SIMPLE_CHECK_ENABLED} = 1; } -use Result::Simple qw( ok err result_for unsafe_unwrap unsafe_unwrap_err chain pipeline ); +use Result::Simple qw( ok err result_for unsafe_unwrap unsafe_unwrap_err chain pipeline combine combine_with_all_errors ); subtest 'Test `ok` and `err` functions' => sub { subtest '`ok` and `err` functions just return values' => sub { @@ -262,4 +262,35 @@ subtest 'Test `pipeline` function' => sub { } }; +subtest 'Test `combine`` function' => sub { + subtest 'All ok()' => sub { + my ($v, $e) = combine( ok(3), ok(4), ok(5) ); + is $v, [3, 4, 5]; + is $e, undef; + }; + + subtest 'When err are included, then return the first error' => sub { + my ($v, $e) = combine( ok(3), err('foo'), ok(5), err('bar') ); + is $v, undef; + is $e, 'foo'; + }; + + like dies { my $v = combine( ok(3), 4, ok(5) ) }, qr/`combine` must be called in list context/; + like dies { my ($v, $e) = combine( ok(3), 4 ) }, qr/`combine` arguments must be Result list/; +}; + +subtest 'Test `combine_with_all_errors` with pipeline' => sub { + subtest 'All ok()' => sub { + my ($v, $e) = combine_with_all_errors( ok(3), ok(4), ok(5) ); + is $v, [3, 4, 5]; + is $e, undef; + }; + + subtest 'When err are included, then return all errors' => sub { + my ($v, $e) = combine_with_all_errors( ok(3), err('foo'), ok(5), err('bar') ); + is $v, undef; + is $e, ['foo', 'bar']; + }; +}; + done_testing;