File Coverage

blib/lib/Perinci/Sub/Util.pm
Criterion Covered Total %
statement 126 160 78.7
branch 47 76 61.8
condition 20 40 50.0
subroutine 13 16 81.2
pod 6 6 100.0
total 212 298 71.1


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