File Coverage

blib/lib/Perinci/Sub/Util.pm
Criterion Covered Total %
statement 141 175 80.5
branch 51 82 62.2
condition 20 40 50.0
subroutine 15 18 83.3
pod 6 6 100.0
total 233 321 72.5


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