File Coverage

blib/lib/Perinci/Sub/Util.pm
Criterion Covered Total %
statement 125 159 78.6
branch 44 74 59.4
condition 16 37 43.2
subroutine 12 15 80.0
pod 6 6 100.0
total 203 291 69.7


line stmt bran cond sub pod time code
1             package Perinci::Sub::Util;
2              
3             our $DATE = '2017-01-31'; # DATE
4             our $VERSION = '0.46'; # VERSION
5              
6 4     4   62537 use 5.010001;
  4         13  
7 4     4   16 use strict;
  4         5  
  4         72  
8 4     4   13 use warnings;
  4         6  
  4         875  
9              
10             require Exporter;
11             our @ISA = qw(Exporter);
12             our @EXPORT_OK = qw(
13             err
14             caller
15             warn_err
16             die_err
17             gen_modified_sub
18             gen_curried_sub
19             );
20              
21             our %SPEC;
22              
23             $SPEC{':package'} = {
24             v => 1.1,
25             summary => 'Helper when writing functions',
26             };
27              
28             our $STACK_TRACE;
29             our @_c; # to store temporary celler() result
30             our $_i; # temporary variable
31             sub err {
32 7     7 1 17029 require Scalar::Util;
33              
34             # get information about caller
35 7         36 my @caller = CORE::caller(1);
36 7 50       18 if (!@caller) {
37             # probably called from command-line (-e)
38 0         0 @caller = ("main", "-e", 1, "program");
39             }
40              
41 7         7 my ($status, $msg, $meta, $prev);
42              
43 7         11 for (@_) {
44 7         8 my $ref = ref($_);
45 7 100       21 if ($ref eq 'ARRAY') { $prev = $_ }
  2 50       22  
    50          
46 0         0 elsif ($ref eq 'HASH') { $meta = $_ }
47             elsif (!$ref) {
48 5 100       12 if (Scalar::Util::looks_like_number($_)) {
49 3         5 $status = $_;
50             } else {
51 2         3 $msg = $_;
52             }
53             }
54             }
55              
56 7   100     19 $status //= 500;
57 7   66     22 $msg //= "$caller[3] failed";
58 7   50     19 $meta //= {};
59 7 100 33     18 $meta->{prev} //= $prev if $prev;
60              
61             # put information on who produced this error and where/when
62 7 50       12 if (!$meta->{logs}) {
63              
64             # should we produce a stack trace?
65 7         6 my $stack_trace;
66             {
67 4     4   17 no warnings;
  4         3  
  4         2332  
  7         4  
68             # we use Carp::Always as a sign that user wants stack traces
69 7 100 33     21 last unless $STACK_TRACE // $INC{"Carp/Always.pm"};
70             # stack trace is already there in previous result's log
71             last if $prev && ref($prev->[3]) eq 'HASH' &&
72             ref($prev->[3]{logs}) eq 'ARRAY' &&
73             ref($prev->[3]{logs}[0]) eq 'HASH' &&
74 2 50 66     18 $prev->[3]{logs}[0]{stack_trace};
      66        
      33        
      33        
75 1         1 $stack_trace = [];
76 1         2 $_i = 1;
77 1         1 while (1) {
78             {
79 10         7 package DB;
80 10         36 @_c = CORE::caller($_i);
81 10 100       14 if (@_c) {
82 9         12 $_c[4] = [@DB::args];
83             }
84             }
85 10 100       12 last unless @_c;
86 9         18 push @$stack_trace, [@_c];
87 9         4 $_i++;
88             }
89             }
90 7         6 push @{ $meta->{logs} }, {
  7         39  
91             type => 'create',
92             time => time(),
93             package => $caller[0],
94             file => $caller[1],
95             line => $caller[2],
96             func => $caller[3],
97             ( stack_trace => $stack_trace ) x !!$stack_trace,
98             };
99             }
100              
101             #die;
102 7         22 [$status, $msg, undef, $meta];
103             }
104              
105             sub warn_err {
106 0     0 1 0 require Carp;
107              
108 0         0 my $res = err(@_);
109 0         0 Carp::carp("ERROR $res->[0]: $res->[1]");
110             }
111              
112             sub die_err {
113 0     0 1 0 require Carp;
114              
115 0         0 my $res = err(@_);
116 0         0 Carp::croak("ERROR $res->[0]: $res->[1]");
117             }
118              
119             sub caller {
120 0     0 1 0 my $n0 = shift;
121 0   0     0 my $n = $n0 // 0;
122              
123 0   0     0 my $pkg = $Perinci::Sub::Wrapper::default_wrapped_package //
124             'Perinci::Sub::Wrapped';
125              
126 0         0 my @r;
127 0         0 my $i = 0;
128 0         0 my $j = -1;
129 0         0 while ($i <= $n+1) { # +1 for this sub itself
130 0         0 $j++;
131 0         0 @r = CORE::caller($j);
132 0 0       0 last unless @r;
133 0 0 0     0 if ($r[0] eq $pkg && $r[1] =~ /^\(eval /) {
134 0         0 next;
135             }
136 0         0 $i++;
137             }
138              
139 0 0       0 return unless @r;
140 0 0       0 return defined($n0) ? @r : $r[0];
141             }
142              
143             $SPEC{gen_modified_sub} = {
144             v => 1.1,
145             summary => 'Generate modified metadata (and subroutine) based on another',
146             description => <<'_',
147              
148             Often you'll want to create another sub (and its metadata) based on another, but
149             with some modifications, e.g. add/remove/rename some arguments, change summary,
150             add/remove some properties, and so on.
151              
152             Instead of cloning the Rinci metadata and modify it manually yourself, this
153             routine provides some shortcuts.
154              
155             You can specify base sub/metadata using `base_name` (string, subroutine name,
156             either qualified or not) or `base_code` (coderef) + `base_meta` (hash).
157              
158             _
159             args => {
160             base_name => {
161             summary => 'Subroutine name (either qualified or not)',
162             schema => 'str*',
163             description => <<'_',
164              
165             If not qualified with package name, will be searched in the caller's package.
166             Rinci metadata will be searched in `%SPEC` package variable.
167              
168             Alternatively, you can also specify `base_code` and `base_meta`.
169              
170             _
171             },
172             base_code => {
173             summary => 'Base subroutine code',
174             schema => 'code*',
175             description => <<'_',
176              
177             If you specify this, you'll also need to specify `base_meta`.
178              
179             Alternatively, you can specify `base_name` instead, to let this routine search
180             the base subroutine from existing Perl package.
181              
182             _
183             },
184             base_meta => {
185             summary => 'Base Rinci metadata',
186             schema => 'hash*', # XXX defhash/rifunc
187             },
188             output_name => {
189             summary => 'Where to install the modified sub',
190             schema => 'str*',
191             description => <<'_',
192              
193             Subroutine will be put in the specified name. If the name is not qualified with
194             package name, will use caller's package. If no `output_code` is specified, the
195             base subroutine reference will be assigned here.
196              
197             Note that this argument is optional.
198              
199             _
200             },
201             output_code => {
202             summary => 'Code for the modified sub',
203             schema => 'code*',
204             description => <<'_',
205              
206             If not specified will use `base_code` (which will then be required).
207              
208             _
209             },
210             summary => {
211             summary => 'Summary for the mod subroutine',
212             schema => 'str*',
213             },
214             description => {
215             summary => 'Description for the mod subroutine',
216             schema => 'str*',
217             },
218             remove_args => {
219             summary => 'List of arguments to remove',
220             schema => 'array*',
221             },
222             add_args => {
223             summary => 'Arguments to add',
224             schema => 'hash*',
225             },
226             replace_args => {
227             summary => 'Arguments to add',
228             schema => 'hash*',
229             },
230             rename_args => {
231             summary => 'Arguments to rename',
232             schema => 'hash*',
233             },
234             modify_args => {
235             summary => 'Arguments to modify',
236             description => <<'_',
237              
238             For each argument you can specify a coderef. The coderef will receive the
239             argument ($arg_spec) and is expected to modify the argument specification.
240              
241             _
242             schema => 'hash*',
243             },
244             modify_meta => {
245             summary => 'Specify code to modify metadata',
246             schema => 'code*',
247             description => <<'_',
248              
249             Code will be called with arguments ($meta) where $meta is the cloned Rinci
250             metadata.
251              
252             _
253             },
254             install_sub => {
255             schema => 'bool',
256             default => 1,
257             },
258             },
259             result => {
260             schema => ['hash*' => {
261             keys => {
262             code => ['code*'],
263             meta => ['hash*'], # XXX defhash/risub
264             },
265             }],
266             },
267             };
268             sub gen_modified_sub {
269 2     2 1 1206 require Function::Fallback::CoreOrPP;
270              
271 2         1370 my %args = @_;
272              
273             # get base code/meta
274 2         4 my ($base_code, $base_meta);
275 2 50       7 if ($args{base_name}) {
    0          
276 2         4 my ($pkg, $leaf);
277 2 50       14 if ($args{base_name} =~ /(.+)::(.+)/) {
278 2         8 ($pkg, $leaf) = ($1, $2);
279             } else {
280 0         0 $pkg = CORE::caller();
281 0         0 $leaf = $args{base_name};
282             }
283 4     4   34 no strict 'refs';
  4         5  
  4         1497  
284 2         4 $base_code = \&{"$pkg\::$leaf"};
  2         10  
285 2         2 $base_meta = ${"$pkg\::SPEC"}{$leaf};
  2         19  
286 2 50       8 die "Can't find Rinci metadata for $pkg\::$leaf" unless $base_meta;
287             } elsif ($args{base_meta}) {
288 0         0 $base_meta = $args{base_meta};
289             $base_code = $args{base_code}
290 0 0       0 or die "Please specify base_code";
291             } else {
292 0         0 die "Please specify base_name or base_code+base_meta";
293             }
294              
295 2         6 my $output_meta = Function::Fallback::CoreOrPP::clone($base_meta);
296 2   66     2903 my $output_code = $args{output_code} // $base_code;
297              
298             # modify metadata
299 2         6 for (qw/summary description/) {
300 4 100       15 $output_meta->{$_} = $args{$_} if $args{$_};
301             }
302 2 50       7 if ($args{remove_args}) {
303 2         3 delete $output_meta->{args}{$_} for @{ $args{remove_args} };
  2         10  
304             }
305 2 100       8 if ($args{add_args}) {
306 1         1 for my $k (keys %{ $args{add_args} }) {
  1         17  
307 1         2 my $v = $args{add_args}{$k};
308             die "Can't add arg '$k' in mod sub: already exists"
309 1 50       3 if $output_meta->{args}{$k};
310 1         2 $output_meta->{args}{$k} = $v;
311             }
312             }
313 2 100       5 if ($args{replace_args}) {
314 1         2 for my $k (keys %{ $args{replace_args} }) {
  1         2  
315 1         2 my $v = $args{replace_args}{$k};
316             die "Can't replace arg '$k' in mod sub: doesn't exist"
317 1 50       3 unless $output_meta->{args}{$k};
318 1         2 $output_meta->{args}{$k} = $v;
319             }
320             }
321 2 100       6 if ($args{rename_args}) {
322 1         2 for my $old (keys %{ $args{rename_args} }) {
  1         1  
323 1         2 my $new = $args{rename_args}{$old};
324 1         2 my $as = $output_meta->{args}{$old};
325 1 50       2 die "Can't rename arg '$old' in mod sub: doesn't exist" unless $as;
326             die "Can't rename arg '$old'->'$new' in mod sub: ".
327 1 50       3 "new name already exist" if $output_meta->{args}{$new};
328 1         2 $output_meta->{args}{$new} = $as;
329 1         2 delete $output_meta->{args}{$old};
330             }
331             }
332 2 100       6 if ($args{modify_args}) {
333 1         1 for (keys %{ $args{modify_args} }) {
  1         3  
334 1         2 $args{modify_args}{$_}->($output_meta->{args}{$_});
335             }
336             }
337 2 100       9 if ($args{modify_meta}) {
338 1         3 $args{modify_meta}->($output_meta);
339             }
340              
341             # install
342 2 100       7 if ($args{output_name}) {
343 1         1 my ($pkg, $leaf);
344 1 50       8 if ($args{output_name} =~ /(.+)::(.+)/) {
345 1         4 ($pkg, $leaf) = ($1, $2);
346             } else {
347 0         0 $pkg = CORE::caller();
348 0         0 $leaf = $args{output_name};
349             }
350 4     4   19 no strict 'refs';
  4         4  
  4         96  
351 4     4   13 no warnings 'redefine';
  4         5  
  4         1395  
352 1 50 50     6 *{"$pkg\::$leaf"} = $output_code if $args{install_sub} // 1;
  1         5  
353 1         1 ${"$pkg\::SPEC"}{$leaf} = $output_meta;
  1         3  
354             }
355              
356 2         12 [200, "OK", {code=>$output_code, meta=>$output_meta}];
357             }
358              
359             $SPEC{gen_curried_sub} = {
360             v => 1.1,
361             summary => 'Generate curried subroutine (and its metadata)',
362             description => <<'_',
363              
364             This is a more convenient helper than `gen_modified_sub` if you want to create a
365             new subroutine that has some of its arguments preset (so they no longer need to
366             be present in the new metadata).
367              
368             For more general needs of modifying a subroutine (e.g. add some arguments,
369             modify some arguments, etc) use `gen_modified_sub`.
370              
371             _
372             args => {
373             base_name => {
374             summary => 'Subroutine name (either qualified or not)',
375             schema => 'str*',
376             description => <<'_',
377              
378             If not qualified with package name, will be searched in the caller's package.
379             Rinci metadata will be searched in `%SPEC` package variable.
380              
381             _
382             req => 1,
383             pos => 0,
384             },
385             set_args => {
386             summary => 'Arguments to set',
387             schema => 'hash*',
388             },
389             output_name => {
390             summary => 'Where to install the modified sub',
391             schema => 'str*',
392             description => <<'_',
393              
394             Subroutine will be put in the specified name. If the name is not qualified with
395             package name, will use caller's package.
396              
397             _
398             req => 1,
399             pos => 2,
400             },
401             },
402             args_as => 'array',
403             result_naked => 1,
404             };
405             sub gen_curried_sub {
406 1     1 1 17 my ($base_name, $set_args, $output_name) = @_;
407              
408 1         4 my $caller = CORE::caller();
409              
410 1         2 my ($base_pkg, $base_leaf);
411 1 50       10 if ($base_name =~ /(.+)::(.+)/) {
412 1         6 ($base_pkg, $base_leaf) = ($1, $2);
413             } else {
414 0         0 $base_pkg = $caller;
415 0         0 $base_leaf = $base_name;
416             }
417              
418 1         2 my ($output_pkg, $output_leaf);
419 1 50       4 if ($output_name =~ /(.+)::(.+)/) {
420 1         2 ($output_pkg, $output_leaf) = ($1, $2);
421             } else {
422 0         0 $output_pkg = $caller;
423 0         0 $output_leaf = $output_name;
424             }
425              
426 1         2 my $base_sub = \&{"$base_pkg\::$base_leaf"};
  1         8  
427              
428             my $res = gen_modified_sub(
429             base_name => "$base_pkg\::$base_leaf",
430             output_name => "$output_pkg\::$output_leaf",
431             output_code => sub {
432 4     4   20 no strict 'refs';
  4         6  
  4         439  
433 1     1   6 $base_sub->(@_, %$set_args);
434             },
435 1         14 remove_args => [keys %$set_args],
436             install => 1,
437             );
438              
439 1 50       4 die "Can't generate curried sub: $res->[0] - $res->[1]"
440             unless $res->[0] == 200;
441              
442 1         3 1;
443             }
444              
445             1;
446             # ABSTRACT: Helper when writing functions
447              
448             __END__