File Coverage

blib/lib/Result/Simple.pm
Criterion Covered Total %
statement 184 184 100.0
branch 78 86 90.7
condition 14 27 51.8
subroutine 38 38 100.0
pod 11 12 91.6
total 325 347 93.6


line stmt bran cond sub pod time code
1             package Result::Simple;
2 3     3   576985 use strict;
  3         6  
  3         91  
3 3     3   35 use warnings;
  3         7  
  3         225  
4              
5             our $VERSION = "0.07";
6              
7 3         21 use Exporter::Shiny qw(
8             ok
9             err
10             result_for
11             chain
12             pipeline
13             combine
14             combine_with_all_errors
15             flatten
16             match
17             unsafe_unwrap
18             unsafe_unwrap_err
19 3     3   1314 );
  3         18026  
20              
21 3     3   214 use Carp;
  3         6  
  3         239  
22 3     3   1783 use Scope::Upper ();
  3         3532  
  3         74  
23 3     3   1426 use Sub::Util ();
  3         790  
  3         97  
24 3     3   18 use Scalar::Util ();
  3         7  
  3         103  
25              
26             # If this option is true, then check `ok` and `err` functions usage and check a return value type.
27             # However, it should be falsy for production code, because of performance, and it is an assertion, not a validation.
28 3   100 3   18 use constant CHECK_ENABLED => $ENV{RESULT_SIMPLE_CHECK_ENABLED} // 1;
  3         10  
  3         309  
29              
30             # err does not allow these values.
31 3     3   14 use constant FALSY_VALUES => [0, '0', '', undef];
  3         5  
  3         2721  
32              
33             # When the function is successful, it should return this.
34             sub ok {
35 55     55 1 725034 if (CHECK_ENABLED) {
36 55 100       359 croak "`ok` must be called in list context" unless wantarray;
37 49 100       180 croak "`ok` does not allow multiple arguments" if @_ > 1;
38 48 100       153 croak "`ok` does not allow no arguments" if @_ == 0;
39             }
40 47         234 ($_[0], undef)
41             }
42              
43             # When the function fails, it should return this.
44             sub err {
45 35     35 1 13780 if (CHECK_ENABLED) {
46 35 100       168 croak "`err` must be called in list context" unless wantarray;
47 27 100       147 croak "`err` does not allow multiple arguments." if @_ > 1;
48 26 100       115 croak "`err` does not allow no arguments" if @_ == 0;
49 25 100       67 croak "`err` does not allow a falsy value: @{[ _ddf($_[0]) ]}" unless $_[0];
  3         10  
50             }
51 22         76 (undef, $_[0])
52             }
53              
54             # result_for foo => (T, E);
55             # This is used to define a function that returns a success or failure.
56             #
57             # Example
58             #
59             # result_for div => Int, ErrorMessage;
60             #
61             # sub div {
62             # my ($a, $b) = @_;
63             # if ($b == 0) {
64             # return err('Division by zero');
65             # }
66             # return ok($a / $b);
67             # }
68             sub result_for {
69 21     21 1 33177 unless (CHECK_ENABLED) {
70             # This is a no-op if CHECK_ENABLED is false.
71             return;
72             }
73              
74 21         71 my ($function_name, $T, $E, %opts) = @_;
75              
76 13   50     71 my @caller = caller($opts{caller_level} || 0);
77 13   33     28 my $package = $opts{package} || $caller[0];
78 13         20 my $filename = $caller[1];
79 13         15 my $line = $caller[2];
80              
81 13         54 my $code = $package->can($function_name);
82              
83 13 100       25 unless ($code) {
84 1         147 croak "result_for: function `$function_name` not found in package `$package` at $filename line $line\n";
85             }
86              
87 12 100 66     40 unless (Scalar::Util::blessed($T) && $T->can('check')) {
88 1         2 croak "result_for T requires `check` method, got: @{[ _ddf($T) ]} at $filename line $line\n";
  1         3  
89             }
90              
91 11 100       18 if (defined $E) {
92 9 100 66     23 unless (Scalar::Util::blessed($E) && $E->can('check')) {
93 1         2 croak "result_for E requires `check` method, got: @{[ _ddf($E) ]} at $filename line $line\n";
  1         3  
94             }
95              
96 8 100       10 if (my @f = grep { $E->check($_) } @{ FALSY_VALUES() }) {
  32         121  
  8         13  
97 1         6 croak "result_for E should not allow falsy values: @{[ _ddf(\@f) ]} at $filename line $line\n";
  1         4  
98             }
99             }
100              
101 9         40 wrap_code($code, $package, $function_name, $T, $E);
102             }
103              
104             # Wrap the original coderef with type check.
105             sub wrap_code {
106 9     9 0 16 my ($code, $package, $name, $T, $E) = @_;
107              
108             my $wrapped = sub {
109 9 100   9   8788 croak "Must handle error in `$name`" unless wantarray;
        9      
        9      
        9      
        9      
        9      
        9      
        9      
        9      
        9      
110              
111 8         71 my @result = &Scope::Upper::uplevel($code, @_, &Scope::Upper::CALLER(0));
112 7 100       21 unless (@result == 2) {
113 2         3 Carp::confess "Invalid result tuple (T, E) in `$name`. Do you forget to call `ok` or `err` function? Got: @{[ _ddf(\@result) ]}";
  2         5  
114             }
115              
116 5         9 my ($data, $err) = @result;
117              
118 5 100       10 if ($err) {
119 2 100       5 if (defined $E) {
120 1 50 33     3 if (!$E->check($err) || defined $data) {
121 1         11 Carp::confess "Invalid failure result in `$name`: @{[ _ddf([$data, $err]) ]}";
  1         3  
122             }
123             } else {
124             # Result(T, undef) should not return an error.
125 1         2 Carp::confess "Never return error in `$name`: @{[ _ddf([$data, $err]) ]}";
  1         4  
126             }
127             } else {
128 3 100 66     46 if (!$T->check($data) || defined $err) {
129 1         11 Carp::confess "Invalid success result in `$name`: @{[ _ddf([$data, $err]) ]}";
  1         4  
130             }
131             }
132              
133 2         32 ($data, $err);
134 9         54 };
135              
136 9         18 my $fullname = "$package\::$name";
137 9         37 Sub::Util::set_subname($fullname, $wrapped);
138              
139 9         20 my $prototype = Sub::Util::prototype($code);
140 9 100       39 if (defined $prototype) {
141 1         12 Sub::Util::set_prototype($prototype, $wrapped);
142             }
143              
144 3     3   20 no strict qw(refs);
  3         21  
  3         125  
145 3     3   13 no warnings qw(redefine);
  3         17  
  3         3422  
146 9         10 *{$fullname} = $wrapped;
  9         40  
147             }
148              
149             # `chain` takes a function name and a result tuple (T, E) and returns a new result tuple (T, E).
150             sub chain {
151 7     7 1 1310 my ($function, $value, $error) = @_;
152              
153 7         11 if (CHECK_ENABLED) {
154 7 100       152 croak "`chain` must be called in list context" unless wantarray;
155 6 100       122 croak "`chain` arguments must be func and result like (func, T, E)" unless @_ == 3;
156             }
157              
158 5 50       9 my $code = ref $function ? $function : do {
159 5         7 my $caller = caller(0);
160 5 100       140 $caller->can($function) or croak "Function `$function` not found in $caller";
161             };
162 4 100       9 return err($error) if $error;
163 3         7 return $code->($value);
164             }
165              
166             # `pipeline` takes a list of function names and returns a new function.
167             sub pipeline {
168 3     3 1 6615 my (@functions) = @_;
169              
170             my @codes = map {
171 3         7 my $f = $_;
  5         9  
172 5 50       16 ref $f ? $f : do {
173 5         10 my $caller = caller(0);
174 5 100       131 $caller->can($f) or croak "Function `$f` not found in $caller";
175             };
176             } @functions;
177              
178             my $pipelined = sub {
179 6     6   1154 my ($value, $error) = @_;
180              
181 6         7 if (CHECK_ENABLED) {
182 6 100       1072 croak "pipelined function must be called in list context" unless wantarray;
183 5 100       115 croak "pipelined function arguments must be result such as (T, E) " unless @_ == 2;
184             }
185              
186 4 50       8 return err($error) if $error;
187 4         8 for my $code (@codes) {
188 7         14 ($value, $error) = $code->($value);
189 6 100       16 return err($error) if $error;
190             }
191 1         3 return ok($value);
192 2         13 };
193              
194 2         58 my $package = caller(0);
195 2         5 my $fullname = "$package\::__PIPELINED_FUNCTION__";
196 2         21 Sub::Util::set_subname($fullname, $pipelined);
197              
198 2         6 return $pipelined;
199             }
200              
201             # `combine` takes a list of Result like `((T1,E1), (T2,E2), (T3,E3))` and returns a new Result like `([T1,T2,T3], E)`.
202             sub combine {
203 4     4 1 9 my @results = @_;
204              
205 4         5 if (CHECK_ENABLED) {
206 4 100       187 croak "`combine` must be called in list context" unless wantarray;
207 3 100       130 croak "`combine` arguments must be Result list" unless @_ % 2 == 0;
208             }
209              
210 2         4 my @values;
211 2         6 for (my $i = 0; $i < @results; $i += 2) {
212 5         10 my ($value, $error) = @results[$i, $i + 1];
213 5 100       8 if ($error) {
214 1         3 return err($error);
215             }
216 4         7 push @values, $value;
217             }
218 1         5 return ok(\@values);
219             }
220              
221             # `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])`.
222             sub combine_with_all_errors {
223 2     2 1 8 my @results = @_;
224              
225 2         3 if (CHECK_ENABLED) {
226 2 50       4 croak "`combine_with_all_errors` must be called in list context" unless wantarray;
227 2 50       6 croak "`combine_with_all_errors` arguments must be Result list" unless @_ % 2 == 0;
228             }
229              
230 2         3 my @values;
231             my @errors;
232 2         6 for (my $i = 0; $i < @results; $i += 2) {
233 7         12 my ($value, $err) = @results[$i, $i + 1];
234 7 100       10 if ($err) {
235 2         4 push @errors, $err;
236             } else {
237 5         9 push @values, $value;
238             }
239             }
240 2 100       28 return err(\@errors) if @errors;
241 1         4 return ok(\@values);
242             }
243              
244             # `flatten` takes a list of Result like `([T1,E1], [T2,E2], [T3,E3])` and returns a new Result like ((T1,E1), (T2,E2), (T3,E3)).
245             sub flatten {
246 2 50 33 2 1 4 map { ref $_ && ref $_ eq 'ARRAY' ? @$_ : $_ } @_;
  6         28  
247             }
248              
249             # `match` takes two coderefs for on success and on failure, and returns a new function.
250             sub match {
251 2     2 1 2493 my ($on_success, $on_failure) = @_;
252              
253 2         5 if (CHECK_ENABLED) {
254 2 50 33     6 croak "`match` arguments must be two coderefs for on success and on error" unless _is_callable($on_success) && _is_callable($on_failure);
255             }
256              
257             my $match = sub {
258 4     4   347 my ($value, $err) = @_;
259              
260 4         6 if (CHECK_ENABLED) {
261 4 100       228 croak "`match` function arguments must be result like (T, E)" unless @_ == 2;
262             }
263              
264 3 100       8 if ($err) {
265 1         3 return $on_failure->($err);
266             } else {
267 2         7 return $on_success->($value);
268             }
269 2         11 };
270              
271 2         4 my $package = caller(0);
272 2         4 my $fullname = "$package\::__MATCHER_FUNCTION__";
273 2         18 Sub::Util::set_subname($fullname, $match);
274              
275 2         6 return $match;
276             }
277              
278             # `unsafe_nwrap` takes a Result and returns a T when the result is an Ok, otherwise it throws exception.
279             # It should be used in tests or debugging code.
280             sub unsafe_unwrap {
281 2     2 1 4 my ($value, $err) = @_;
282 2 100       6 if ($err) {
283 1         3 croak "Error called in `unsafe_unwrap`: @{[ _ddf($err) ]}"
  1         4  
284             }
285 1         27 return $value;
286             }
287              
288             # `unsafe_unwrap_err` takes a Result and returns an E when the result is an Err, otherwise it throws exception.
289             # It should be used in tests or debugging code.
290             sub unsafe_unwrap_err {
291 2     2 1 3 my ($value, $err) = @_;
292 2 100       6 if (!$err) {
293 1         2 croak "No error called in `unsafe_unwrap_err`: @{[ _ddf($value) ]}"
  1         3  
294             }
295 1         2 return $err;
296             }
297              
298             # Dump data for debugging.
299             sub _ddf {
300 13     13   20 my $v = shift;
301              
302 3     3   24 no warnings 'once';
  3         7  
  3         515  
303 13         795 require Data::Dumper;
304 13         6530 local $Data::Dumper::Indent = 0;
305 13         21 local $Data::Dumper::Useqq = 0;
306 13         14 local $Data::Dumper::Terse = 1;
307 13         17 local $Data::Dumper::Sortkeys = 1;
308 13         15 local $Data::Dumper::Maxdepth = 2;
309 13         36 Data::Dumper::Dumper($v);
310             }
311              
312             # Check if the argument is a callable.
313             sub _is_callable {
314 4     4   6 my $code = shift;
315 4   50     21 (Scalar::Util::reftype($code)||'') eq 'CODE'
316             }
317              
318             1;
319             __END__