File Coverage

blib/lib/Process/Results.pm
Criterion Covered Total %
statement 100 113 88.5
branch 29 46 63.0
condition 5 11 45.4
subroutine 23 24 95.8
pod 11 20 55.0
total 168 214 78.5


line stmt bran cond sub pod time code
1             package Process::Results;
2 1     1   514 use strict;
  1         2  
  1         37  
3 1     1   4 use Carp 'croak';
  1         1  
  1         57  
4 1     1   6 use B;
  1         4  
  1         47  
5 1     1   460 use JSON::Tiny;
  1         12052  
  1         1156  
6              
7             # debug tools
8             # use Debug::ShowStuff ':all';
9             # use Debug::ShowStuff::ShowVar;
10              
11             # version
12             our $VERSION = '0.1';
13              
14             # config
15             my $tab = "\t";
16              
17              
18             #------------------------------------------------------------------------------
19             # pod
20             #
21              
22             =head1 NAME
23              
24             Process::Results - standardized structure for returning results of a process
25              
26             =head1 SYNOPSIS
27              
28             use Process::Results;
29            
30             my $results = Process::Results->new();
31            
32             some_subroutine(results=>$results) {
33             ...
34             }
35            
36             if ($results->success) {}
37             else {}
38            
39             much more...
40              
41             =head1 OVERVIEW
42              
43             Getting the details about the results of a subroutine call can be challenging.
44             It's easy enough for a subroutine to indicate if it succeeded or not, or to
45             simply die or croak. Communicating more detail, however, can get complicated.
46             What was the cause of the failure? What was the input value that caused it?
47             Maybe there were B problems, any of which could have independently
48             caused a failure.
49              
50             Furthermore, it's not just failures that need communicating. Maybe there were
51             results of the process that need to be communicated back to the caller, in
52             addition to the success or failure of the operation.
53              
54             Process::Results provides a standardized way for caller and subroutine to
55             communicate complex details of an operation. A Process::Results object is
56             passed into the subroutine call, which can then store results information in
57             the object. The sub doesn't even have to return the object because the caller
58             still has a reference to it.
59              
60             Keep in mind that a process doesn't have to return the results object, so your
61             sub can still return success, failure, or some other value without the caller
62             having to check the Results object. In many cases, a successful process doesn't
63             need to provide any details - it's only on failure that details are needed.
64              
65             At its simplest, a Results object is just an empty hash. By default, an empty
66             hash indicates success, which can be checked with the success method:
67              
68             $results->success()
69              
70             If you prefer, you can check for failure, which just returns the opposite of
71             success():
72              
73             $results->failure()
74              
75             If you prefer that the results object defaults to false, just add a 'success'
76             option when creating the new object:
77              
78             $results = Process::Results->new(success=>0);
79             $results->success(); # returns false
80              
81             In a more complex situation, the results object might contain one or more
82             messages in the errors array. Such an object would look like this:
83              
84             {
85             errors => [
86             { id=>'file-open-error', path=>'/tmp/output.txt' },
87             { id=>'missing-param', param_name=>'email' },
88             ]
89             }
90              
91             The presence of any elements in C means that the process failed, so
92             C<$results-Esuccess()> returns false. A complete explanation of the
93             structure of a results object is in the next section.
94              
95             =head2 Structure
96              
97             A complete structure of a results object looks like this:
98              
99             {
100             success => 0,
101             errors => [
102             { id=>'file-open-error', path=>'/tmp/output.txt' },
103             { id=>'missing-param', param_name=>'email' },
104             ],
105             warnings => [
106             # more messages here
107             ],
108             notes => [
109             # more messages here
110             ],
111             details => {
112             # a hash that can contain anything you want
113             }
114             }
115              
116             The C and C properties are redundant: the presence of any
117             errors indicates failure. If both properties are present, C overrides
118             C.
119              
120             Errors indicate that the process failed. Warnings do not indicate a failure,
121             but do indicate that something went wrong. Notes are simply information about
122             the process and don't mean anything was wrong at all.
123              
124             =head2 Message objects
125              
126             Each message is a hash reference. Each message object must have the C
127             property. Other properties can provide details about the message, for example
128             a problematic input param. You can create message objects with the
129             C, C, and C methods:
130              
131             $results->error('file-not-found');
132             $results->warning('very-long-loop');
133             $results->warning('new-id');
134              
135             More on those details below.
136              
137             =head1 METHODS
138              
139             =cut
140              
141             #
142             # pod
143             #------------------------------------------------------------------------------
144              
145              
146             #------------------------------------------------------------------------------
147             # new
148             #
149              
150             =head2 new()
151              
152             Cnew()> creates a new Process::Results object. By default,
153             the object is an empty hash.
154              
155             my $results = Process::Results->new(); # returns empty, blessed hashref
156              
157             B
158              
159             =over
160              
161             =item * success
162              
163             The C option sets an explicit success or failure for the new object.
164             By default, you might want your results object to fail by default. In that case
165             you could do the following:
166              
167             $results = Process::Results->new(success=>0);
168            
169             # stuff happens, but nothing affects the results object
170              
171             $results->success(); # returns false
172              
173             =item * json
174              
175             You can pass in a json string which will be parsed and used to populate the new
176             object. For example:
177              
178             $results = Process::Results->new(json=>'{"errors":[{"id":"no-file"}]}');
179              
180             produces this structure:
181              
182             {
183             errors => [
184             {
185             id => "no-file"
186             }
187             ]
188             }
189              
190             =item * results
191              
192             C can return an existing results object if the C option is
193             sent. This option is handy when you want to ensure that your subroutine has a
194             results object regardless of whether or not one was passed in. For example,
195             consider the following sub:
196              
197             sub mysub {
198             my ($param, %opts) = @_;
199             my $results = Process::Results->new(results=>$opts{'results'});
200            
201             # [do stuff]
202             }
203              
204             In that example, the caller can send in a results object with the options hash.
205             If it does so, that result object is used. If no such option is sent, the sub
206             has a new results object to use.
207              
208             If the C object is sent, all other options are ignored.
209              
210             =back
211              
212             =cut
213              
214             sub new {
215 16     16 1 6411 my ($class, %opts) = @_;
216 16         19 my ($results);
217            
218             # TESTING
219             # println subname(); ##i
220            
221             # if another results object was sent in options, return that
222 16 100       40 if ( $opts{'results'} ) {
223 1 50       7 if (UNIVERSAL::isa $opts{'results'}, 'Process::Results') {
224 1         5 return $opts{'results'};
225             }
226             }
227            
228             # if json was sent, parse it
229 15 100       27 if ( $opts{'json'} ) {
230 1         7 $results = JSON::Tiny::decode_json($opts{'json'});
231             }
232            
233             # else just create empty hashref
234             else {
235 14         24 $results = {};
236             }
237            
238             # set explicit success if it was sent
239 15 100       414 if (exists $opts{'success'}) {
240 1 50       5 $results->{'success'} = $opts{'success'}? 1 : 0;
241             }
242            
243             # bless object
244 15         23 $results = bless($results, $class);
245            
246             # return
247 15         36 return $results;
248             }
249             #
250             # new
251             #------------------------------------------------------------------------------
252              
253              
254             #------------------------------------------------------------------------------
255             # messages
256             #
257              
258             =head2 error(), warning(), note()
259              
260             Each of these methods creates a message object (which is just a hashref) for
261             their respective category. The single required param is an id for the message.
262             The id can be any defined string that you want. For example, the following code
263             creates an error object with the id "do-not-find-file".
264              
265             $results->error('do-not-find-file');
266              
267             That code creates a message object, stored in the C array, with the
268             following structure:
269              
270             {
271             'id' => 'do-not-find-file'
272             }
273              
274             A message object can hold any other properties you want. Those properties
275             should give the details of the message. Those properties can be set with
276             additional params to the method call. So, for example, the following code
277             sets an error with the id "do-not-find-file", along with indicating the path
278             that does not have the file:
279              
280             $results->error('do-not-find-file', path=>$file_path);
281              
282             which would result in an object like this:
283              
284             {
285             'id' => 'do-not-find-file',
286             'path' => '/tmp/data.txt'
287             }
288              
289             The message method returns the message object, so if you prefer you can set
290             those properties directly in the message object, like this:
291              
292             $msg = $results->error('do-not-find-file');
293             $msg->{'path'} = $file_path;
294              
295             =cut
296              
297             sub message {
298 7     7 0 15 my ($results, $type, $id, %opts) = @_;
299 7         7 my ($msg);
300            
301             # TESTING
302             # println subname(); ##i
303            
304             # ensure resutls object has message type
305 7   50     46 $results->{$type} ||= [];
306            
307             # build message object
308 7         19 $msg = { id=>$id, %opts };
309            
310             # add to array
311 7         8 push @{$results->{$type}}, $msg;
  7         15  
312            
313             # return message
314 7         21 return $msg;
315             }
316              
317 5     5 1 32 sub error { return shift->message('errors', @_) }
318 1     1 1 10 sub warning { return shift->message('warnings', @_) }
319 1     1 1 11 sub note { return shift->message('notes', @_) }
320             #
321             # messages
322             #------------------------------------------------------------------------------
323              
324              
325             #------------------------------------------------------------------------------
326             # success
327             #
328              
329             =head2 success()
330              
331             C<$results-Esuccess()> returns true or false to indicate the success state of
332             the process. Success is determined in one of two ways: if the C
333             property is defined, then the boolean value of that property is returned.
334             Else, if there are any messages in the C array, then false is returned,
335             else true is returned. C always returns either 1 or 0.
336              
337             Here are some examples of some results objects and what C returns:
338              
339             # empty hash returns true
340             {}
341            
342             # defined, false value of the success property returns false
343             { 'success'=>0 }
344            
345             # errors array with at least one message returns false
346             {
347             'errors'=>[
348             {'id'=>'do-not-find-file'}
349             ],
350             }
351            
352             # If there is a conflict between explicit success and the errors array, then
353             # the explicit success is returned. That's confusing, so try to avoid that.
354             {
355             'success'=>1,
356             'errors'=>[
357             {'id'=>'do-not-find-file'}
358             ],
359             }
360              
361             =cut
362              
363             sub success {
364 18     18 1 2495 my ($results) = @_;
365            
366             # if success has been explcitly defined, use that
367 18 100       43 if (defined $results->{'success'}) {
368 9 100       83 return $results->{'success'} ? 1 : 0;
369             }
370            
371             # else calculate success from errors array
372             else {
373 9         11 my $errs = $results->{'errors'};
374            
375 9 100 66     42 if ( $errs && UNIVERSAL::isa($errs, 'ARRAY')) {
376 4 50       9 if (@$errs)
377 4         21 { return 0 }
378             else
379 0         0 { return 1 }
380             }
381             else {
382 5         27 return 1;
383             }
384             }
385             }
386             #
387             # success
388             #------------------------------------------------------------------------------
389              
390              
391             #------------------------------------------------------------------------------
392             # failure
393             #
394              
395             =head2 failure()
396              
397             C<$results-Efailure()> simply returns the boolean opposite of
398             C<$results-Esuccess()>. C<$results-Efailure()> always returns 1 or 0.
399              
400             =cut
401              
402             sub failure {
403 4 100   4 1 12 return $_[0]->success ? 0 : 1;
404             }
405             #
406             # failure
407             #------------------------------------------------------------------------------
408              
409              
410             #------------------------------------------------------------------------------
411             # succeed, fail
412             #
413              
414             =head2 succeed(), fail()
415              
416             C<$results-Esucceed()> and C<$results-Efail()> explicitly set the
417             success state of the results object. All they do is set the C
418             property to 1 (C) or 0 (C).
419              
420             =cut
421              
422             sub succeed {
423 1     1 1 6 $_[0]->{'success'} = 1;
424             }
425              
426             sub fail {
427 2     2 1 21 $_[0]->{'success'} = 0;
428             }
429              
430             #
431             # succeed, fail
432             #------------------------------------------------------------------------------
433              
434              
435             #------------------------------------------------------------------------------
436             # unsucceed, unfail
437             #
438              
439             =head2 unsucceed(), unfail()
440              
441             C<$results-Eunsucceed()> and C<$results-Eunfail()> do the same thing:
442             delete the C proeperty.
443              
444             =cut
445              
446             sub unsucceed {
447 1     1 1 3 delete $_[0]->{'success'};
448             }
449              
450             sub unfail {
451 0     0 1 0 delete $_[0]->{'success'};
452             }
453              
454             #
455             # succeed, fail
456             #------------------------------------------------------------------------------
457              
458              
459             #------------------------------------------------------------------------------
460             # json
461             #
462              
463             =head2 json()
464              
465             C<$results-Ejson()> returns a JSON representation of the results object.
466             That's all, it takes no params, it just returns a JSON string.
467              
468             OK, one minor thing to note is that the C property is set to the JSON
469             value of C or C. Other then that, nothing complicated.
470              
471             =cut
472              
473             sub json {
474 1     1 1 6 my ($results) = @_;
475 1         1 my ($success, %calc);
476            
477             # make a copy of the object
478 1         5 %calc = %$results;
479            
480             # set success property
481 1 50       7 if (defined $calc{'success'}) {
482             $calc{'success'} =
483 0 0       0 $calc{'success'} ?
484             JSON::Tiny::true() :
485             JSON::Tiny::false();
486             }
487            
488             # return
489 1         5 return to_json(\%calc);
490             }
491             #
492             # json
493             #------------------------------------------------------------------------------
494              
495              
496             #------------------------------------------------------------------------------
497             # to_json
498             # private method
499             #
500             sub to_json {
501 1     1 0 3 my ($object) = @_;
502 1         1 my ($json);
503            
504             # TESTING
505             # println subname(); ##i
506            
507             # intialize string
508 1         3 $$json = '';
509            
510             # output object
511 1         4 to_json_object($object, 0, $json);
512            
513             # return
514 1         2 return $$json;
515             }
516             #
517             # to_json
518             #------------------------------------------------------------------------------
519              
520              
521              
522             #------------------------------------------------------------------------------
523             # to_json_object
524             # private method
525             #
526             sub to_json_object {
527 4     4 0 6 my ($object, $depth, $json) = @_;
528            
529             # TESTING
530             # println subname(); ##i
531            
532             # hash
533 4 100       24 if ( UNIVERSAL::isa $object, 'HASH' ) {
    100          
    50          
    50          
534 2         7 to_json_hash($object, $depth, $json);
535             }
536            
537             # array
538             elsif ( UNIVERSAL::isa $object, 'ARRAY' ) {
539 1         3 to_json_array($object, $depth, $json);
540             }
541            
542             # JSON::Tiny::_Bool
543             elsif ( UNIVERSAL::isa $object, 'JSON::Tiny::_Bool' ) {
544 0 0       0 if ( $object )
545 0         0 { $$json .= 'true' }
546             else
547 0         0 { $$json .= 'false' }
548             }
549            
550             # other unknown object
551             elsif (ref $object) {
552 0         0 croak 'unknown-object-type: unable to parse object type ' . ref($object);
553             }
554            
555             # else scalar
556             else {
557 1         3 $$json .= json_quote($object);
558             }
559             }
560             #
561             # to_json_object
562             #------------------------------------------------------------------------------
563              
564              
565             #------------------------------------------------------------------------------
566             # to_json_hash
567             # private method
568             #
569             sub to_json_hash {
570 2     2 0 3 my ($hash, $depth, $json) = @_;
571 2         20 my (@keys, $depth_local);
572            
573             # TESTING
574             # println subname(); ##i
575            
576             # indent
577 2         4 $depth_local = $depth+1;
578            
579             # begin hash
580 2         3 $$json .= "{\n";
581            
582             # array of keys to output
583 2         6 @keys = hash_keys($hash);
584            
585             # loop through keys
586 2         8 for (my $idx=0; $idx < @keys; $idx++) {
587 2         3 my $key = $keys[$idx];
588            
589             # output key
590 2         9 $$json .= ($tab x $depth_local) . json_quote($key) . ' : ';
591            
592             # output value
593 2         12 to_json_object($hash->{$key}, $depth_local, $json);
594            
595             # add comma if this isn't the last element
596 2 50       6 if ($idx < (@keys-1))
597 0         0 { $$json .= ',' }
598            
599             # close key
600 2         4 $$json .= "\n";
601             }
602            
603             # end hash
604 2         6 $$json .= ($tab x $depth) . "}";
605             }
606             #
607             # to_json_hash
608             #------------------------------------------------------------------------------
609              
610              
611             #------------------------------------------------------------------------------
612             # hash_keys
613             # private method
614             #
615             our @first_keys = (
616             'success',
617             'success-explicit',
618             );
619              
620             sub hash_keys {
621 2     2 0 3 my ($hash) = @_;
622 2         2 my (%all, @rv);
623            
624             # TESTING
625             # println subname(); ##i
626            
627             # build hash of keys
628 2         6 @all{keys %$hash} = ();
629            
630             # first keys
631 2         6 foreach my $first (@first_keys) {
632 4 50       11 if ( exists $all{$first} ) {
633 0         0 delete $all{$first};
634 0         0 push @rv, $first;
635             }
636             }
637            
638             # append rest of keys to @keys
639 2         5 push @rv, keys(%all);
640            
641             # return
642 2         6 return @rv;
643             }
644             #
645             # hash_keys
646             #------------------------------------------------------------------------------
647              
648              
649              
650             #------------------------------------------------------------------------------
651             # to_json_array
652             # private method
653             #
654             sub to_json_array {
655 1     1 0 2 my ($array, $depth, $json) = @_;
656 1         3 my ($depth_local);
657            
658             # TESTING
659             # println subname(); ##i
660            
661             # indent
662 1         1 $depth_local = $depth+1;
663            
664             # begin array
665             # $$json .= ($tab x $depth) . "[\n";
666 1         2 $$json .= "[\n";
667            
668             # loop through elements
669 1         6 for (my $idx=0; $idx < @$array; $idx++) {
670             # indent
671 1         3 $$json .= ($tab x $depth_local);
672            
673             # output value
674 1         3 to_json_object($array->[$idx], $depth_local, $json);
675            
676             # add comma if this isn't the last element
677 1 50       2 if ($idx < (@$array-1))
678 0         0 { $$json .= ',' }
679            
680             # close key
681 1         3 $$json .= "\n";
682             }
683            
684             # end array
685 1         2 $$json .= ($tab x $depth) . "]";
686             }
687             #
688             # to_json_array
689             #------------------------------------------------------------------------------
690              
691              
692             #------------------------------------------------------------------------------
693             # json_quote
694             # private method
695             #
696             sub json_quote {
697 3     3 0 4 my ($val) = @_;
698            
699             # if it's undef, return null
700 3 50       5 if (! defined $val)
701 0         0 { return 'null' }
702            
703             # if it's a number, return as is
704 3 50       5 if ( is_number($val) )
705 0         0 { return $val }
706            
707             # else return quoted
708 3         4 return encode_string($val);
709             }
710             #
711             # json_quote
712             #------------------------------------------------------------------------------
713              
714              
715             #------------------------------------------------------------------------------
716             # encode_string
717             # private method
718             # This code is copied rote from JSON::Tiny.
719             #
720             my %ESCAPE = (
721             '"' => '"',
722             '\\' => '\\',
723             '/' => '/',
724             'b' => "\x08",
725             'f' => "\x0c",
726             'n' => "\x0a",
727             'r' => "\x0d",
728             't' => "\x09",
729             'u2028' => "\x{2028}",
730             'u2029' => "\x{2029}"
731             );
732              
733             my %REVERSE = map { $ESCAPE{$_} => "\\$_" } keys %ESCAPE;
734              
735             sub encode_string {
736 3     3 0 3 my $str = shift;
737 3         4 $str =~ s!([\x00-\x1f\x{2028}\x{2029}\\"/])!$REVERSE{$1}!gs;
738 3         10 return "\"$str\"";
739             }
740             #
741             # encode_string
742             #------------------------------------------------------------------------------
743              
744              
745              
746             #------------------------------------------------------------------------------
747             # is_number
748             # private method
749             # This code is copied rote from JSON::Tiny.
750             #
751             sub is_number {
752 3     3 0 3 my ($value) = @_;
753            
754             # return true if number
755 3 0 33     31 return 1
      33        
756             if B::svref_2object(\$value)->FLAGS & (B::SVp_IOK | B::SVp_NOK)
757             && 0 + $value eq $value
758             && $value * 0 == 0;
759            
760             # else return false
761 3         7 return 0;
762             }
763             #
764             # is_number
765             #------------------------------------------------------------------------------
766              
767             # return
768             1;
769              
770             __END__