File Coverage

blib/lib/Perinci/Sub/ValidateArgs.pm
Criterion Covered Total %
statement 135 142 95.0
branch 50 64 78.1
condition 27 38 71.0
subroutine 15 15 100.0
pod 1 1 100.0
total 228 260 87.6


line stmt bran cond sub pod time code
1             package Perinci::Sub::ValidateArgs;
2              
3             # NOIFBUILT
4             our $DATE = '2019-04-15'; # DATE
5             our $VERSION = '0.011'; # VERSION
6              
7 1     1   1507 use 5.010001;
  1         14  
8 1     1   5 use strict 'subs', 'vars';
  1         2  
  1         25  
9 1     1   4 use warnings;
  1         1  
  1         20  
10              
11 1     1   404 use Data::Dmp;
  1         1552  
  1         52  
12              
13 1     1   6 use Exporter qw(import);
  1         2  
  1         1347  
14             our @EXPORT_OK = qw(gen_args_validator);
15              
16             # XXX cache key should also contain data_term
17             #my %dsah_compile_cache; # key = schema (C<string> or R<refaddr>), value = compilation result
18              
19             our %SPEC;
20              
21             $SPEC{gen_args_validator} = {
22             v => 1.1,
23             summary => 'Generate argument validator from Rinci function metadata',
24             args => {
25             meta => {
26             schema => 'hash*', # XXX rinci::function_meta
27             description => <<'_',
28              
29             If not specified, will be searched from caller's `%SPEC` package variable.
30              
31             _
32             },
33             source => {
34             summary => 'Whether we want to get the source code instead',
35             schema => 'bool',
36             description => <<'_',
37              
38             The default is to generate Perl validator code, compile it with `eval()`, and
39             return the resulting coderef. When this option is set to true, the generated
40             source string will be returned instead.
41              
42             _
43             },
44             die => {
45             summary => 'Whether validator should die or just return '.
46             'an error message/response',
47             schema => 'bool',
48             },
49             },
50             result_naked => 1,
51             };
52             sub gen_args_validator {
53 8     8 1 22298 my %args = @_;
54              
55 8         20 my $meta = $args{meta};
56 8 100       25 unless ($meta) {
57 7 50       53 my @caller = caller(1) or die "Call gen_args_validator() inside ".
58             "your function or provide 'meta'";
59 7         59 my ($pkg, $func) = $caller[3] =~ /(.+)::(.+)/;
60 7 50       15 $meta = ${"$pkg\::SPEC"}{$func}
  7         40  
61             or die "No metadata for $caller[3]";
62             }
63 8   100     37 my $args_as = $meta->{args_as} // 'hash';
64 8   100     24 my $meta_args = $meta->{args} // {};
65 8         35 my @meta_args = sort keys %$meta_args;
66              
67 8         32 my @code;
68             my @modules_for_all_args;
69 8         0 my @mod_stmts;
70              
71 8         0 my $use_dpath;
72              
73             my $gencode_err = sub {
74 26     26   59 my ($status, $term_msg) = @_;
75 26 100       74 if ($args{die}) {
    100          
76 4         16 return "die $term_msg;";
77             } elsif ($meta->{result_naked}) {
78             # perhaps if result_naked=1, die by default?
79 4         14 return "return $term_msg;";
80             } else {
81 18         79 return "return [$status, $term_msg];";
82             }
83 8         39 };
84             my $addcode_validator = sub {
85 14     14   23 state $plc = do {
86 7         914 require Data::Sah;
87 7         6289 Data::Sah->new->get_compiler("perl");
88             };
89 14         37752 my ($schema, $data_name, $data_term) = @_;
90 14         22 my $cd;
91 14 100       50 my $cache_key = ref($schema) ? "R$schema" : "S$schema";
92             #unless ($cd = $dsah_compile_cache{$cache_key}) {
93 14         53 $cd = $plc->compile(
94             schema => $schema,
95             data_name => $data_name,
96             data_term => $data_term,
97             err_term => '$err',
98             return_type => 'str',
99             indent_level => 2,
100             );
101 14 50       139525 die "Incompatible Data::Sah version (cd v=$cd->{v}, expected 2)" unless $cd->{v} == 2;
102             # $dsah_compile_cache{$cache_key} = $cd;
103             #}
104 14         40 push @code, " \$err = undef;\n";
105 14 100       40 push @code, " \$_sahv_dpath = [];\n" if $cd->{use_dpath};
106 14         30 push @code, " unless (\n";
107 14         54 push @code, $cd->{result}, ") { ".$gencode_err->(400, "\"Validation failed for argument '$data_name': \$err\"")." }\n";
108 14         25 for my $mod_rec (@{ $cd->{modules} }) {
  14         38  
109 54 100       206 next unless $mod_rec->{phase} eq 'runtime';
110 34 100       60 next if grep { ($mod_rec->{use_statement} && $_->{use_statement} && $_->{use_statement} eq $mod_rec->{use_statement}) ||
111 53 100 100     251 $_->{name} eq $mod_rec->{name} } @modules_for_all_args;
      66        
112 20         30 push @modules_for_all_args, $mod_rec;
113 20         60 push @mod_stmts, $plc->stmt_require_module($mod_rec)."\n";
114             }
115 14 100       207 if ($cd->{use_dpath}) {
116 6         117 $use_dpath = 1;
117             }
118 8         36 };
119              
120 8 100 100     75 if ($args_as eq 'hash' || $args_as eq 'hashref') {
    50 66        
121 5         13 push @code, " # check unknown args\n";
122 5         14 push @code, " for (keys %\$args) { unless (/\\A(".join("|", map { quotemeta } @meta_args).")\\z/) { ".$gencode_err->(400, '"Unknown argument \'$_\'"')." } }\n";
  8         44  
123 5         15 push @code, "\n";
124              
125 5         11 for my $arg_name (@meta_args) {
126 8         19 my $arg_spec = $meta_args->{$arg_name};
127 8         19 my $term_arg = "\$args->{'$arg_name'}";
128 8         49 push @code, " # check argument $arg_name\n";
129 8 100       24 if (defined $arg_spec->{default}) {
130 4         17 push @code, " $term_arg //= ".dmp($arg_spec->{default}).";\n";
131             }
132 8         261 push @code, " if (exists $term_arg) {\n";
133 8 50       33 $addcode_validator->($arg_spec->{schema}, $arg_name, $term_arg) if $arg_spec->{schema};
134 8 100       25 if ($arg_spec->{req}) {
135 4         11 push @code, " } else {\n";
136 4         16 push @code, " ".$gencode_err->(400, "\"Missing required argument '$arg_name'\"")."\n";
137             }
138 8         26 push @code, " }\n";
139             }
140              
141 5 100       19 push @code, "\n" if @meta_args;
142             } elsif ($args_as eq 'array' || $args_as eq 'arrayref') {
143             # map the arguments' position
144             my @arg_names = sort {
145 3   50     17 ($meta_args->{$a}{pos}//9999) <=> ($meta_args->{$b}{pos}//9999)
  3   50     22  
146             } keys %$meta_args;
147 3 100 66     25 if (@arg_names && ($meta_args->{$arg_names[-1]}{slurpy} // $meta_args->{$arg_names[-1]}{greedy})) {
      66        
148 2         28 my $pos = @arg_names - 1;
149 2         6 push @code, " # handle slurpy last arg\n";
150 2         10 push @code, " if (\@\$args >= $pos) { \$args->[$pos] = [splice \@\$args, $pos] }\n\n";
151             }
152              
153 3         5 my $start_of_optional;
154 3         9 for my $i (0..$#arg_names) {
155 6         11 my $arg_name = $arg_names[$i];
156 6         10 my $arg_spec = $meta_args->{$arg_name};
157 6 100       13 if ($arg_spec->{req}) {
158 3 50       10 if (defined $start_of_optional) {
159 0         0 die "Error in metadata: after a param is optional ".
160             "(#$start_of_optional) the rest (#$i) must also be optional";
161             }
162             } else {
163 3   33     17 $start_of_optional //= $i;
164             }
165             }
166              
167 3         7 push @code, " # check number of args\n";
168 3 50       9 if ($start_of_optional) {
    0          
169 3         20 push @code, " if (\@\$args < $start_of_optional || \@\$args > ".(@arg_names).") { ".$gencode_err->(400, "\"Wrong number of arguments (expected $start_of_optional..".(@arg_names).", got \".(\@\$args).\")\"") . " }\n";
170             } elsif (defined $start_of_optional) {
171 0         0 push @code, " if (\@\$args > ".(@arg_names).") { ".$gencode_err->(400, "\"Wrong number of arguments (expected 0..".(@arg_names).", got \".(\@\$args).\")\"") . " }\n";
172             } else {
173 0         0 push @code, " if (\@\$args != ".(@arg_names).") { ".$gencode_err->(400, "\"Wrong number of arguments (expected ".(@arg_names).", got \".(\@\$args).\")\"") . " }\n";
174             }
175 3         8 push @code, "\n";
176              
177 3         7 for my $i (0..$#arg_names) {
178 6         11 my $arg_name = $arg_names[$i];
179 6         12 my $arg_spec = $meta_args->{$arg_name};
180 6         13 my $term_arg = "\$args->[$i]";
181 6 50 66     42 if (!defined($arg_spec->{pos})) {
    50 66        
    50          
182 0         0 die "Error in metadata: argument '$arg_name' does not ".
183             "have pos property set";
184             } elsif ($arg_spec->{pos} != $i) {
185 0         0 die "Error in metadata: argument '$arg_name' does not ".
186             "the correct pos value ($arg_spec->{pos}, should be $i)";
187             } elsif (($arg_spec->{slurpy} // $arg_spec->{greedy}) && $i < $#arg_names) {
188 0         0 die "Error in metadata: argument '$arg_name' has slurpy=1 ".
189             "but is not the last argument";
190             }
191 6         16 push @code, " # check argument $arg_name\n";
192 6 100       16 if (defined $arg_spec->{default}) {
193 3         12 push @code, " $term_arg //= ".dmp($arg_spec->{default}).";\n";
194             }
195 6         145 my $open_block;
196 6 100 66     23 if (defined($start_of_optional) && $i >= $start_of_optional) {
197 3         5 $open_block++;
198 3         8 push @code, " if (\@\$args > $i) {\n";
199             }
200 6 50       22 $addcode_validator->($arg_spec->{schema}, $arg_name, $term_arg) if $arg_spec->{schema};
201 6 100       17 push @code, " }\n" if $open_block;
202              
203 6         21 push @code, "\n";
204             }
205             } else {
206 0         0 die "Unsupported args_as '$args_as'";
207             }
208 8         19 push @code, " return undef;\n";
209 8         18 push @code, "}\n";
210              
211 8         37 unshift @code, (
212             "sub {\n",
213             " my \$args = shift;\n",
214             " my \$err;\n",
215             (" my \$_sahv_dpath;\n") x !!$use_dpath,
216             "\n"
217             );
218              
219 8         60 my $code = join("", @mod_stmts, @code);
220 8 100       28 if ($args{source}) {
221 1         18 return $code;
222             } else {
223             #use String::LineNumber 'linenum'; say linenum $code;
224 1     1   8 my $sub = eval $code;
  1     1   3  
  1     1   545  
  1     1   8  
  1     1   2  
  1     1   783  
  1     1   9  
  1         2  
  1         520  
  1         8  
  1         2  
  1         496  
  1         7  
  1         2  
  1         229  
  1         9  
  1         2  
  1         471  
  1         9  
  1         2  
  1         511  
  7         525  
225 7 50       28 die if $@;
226 7         168 return $sub;
227             }
228             }
229              
230             1;
231             # ABSTRACT: Validate function arguments using schemas in Rinci function metadata
232              
233             __END__
234              
235             =pod
236              
237             =encoding UTF-8
238              
239             =head1 NAME
240              
241             Perinci::Sub::ValidateArgs - Validate function arguments using schemas in Rinci function metadata
242              
243             =head1 VERSION
244              
245             This document describes version 0.011 of Perinci::Sub::ValidateArgs (from Perl distribution Perinci-Sub-ValidateArgs), released on 2019-04-15.
246              
247             =head1 SYNOPSIS
248              
249             use Perinci::Sub::ValidateArgs qw(gen_args_validator);
250              
251             our %SPEC;
252             $SPEC{foo} = {
253             v => 1.1,
254             args => {
255             a1 => {
256             schema => 'int*',
257             req => 1,
258             },
259             a2 => {
260             schema => [array => of=>'int*'],
261             default => 'peach',
262             },
263             },
264             'x.func.validate_args' => 1,
265             };
266             sub foo {
267             state $validator = gen_args_validator();
268             my %args = @_;
269             if (my $err = $validator->(\%args)) { return $err }
270              
271             ...
272             }
273              
274             or, if you want the validator to die on failure:
275              
276             ...
277             sub foo {
278             state $validator = gen_args_validator(die => 1);
279             my %args = @_;
280             $validator->(\%args);
281              
282             ...
283             }
284              
285             =head1 DESCRIPTION
286              
287             This module (PSV for short) can be used to validate function arguments using
288             schema information in Rinci function metadata.
289              
290             There are other ways if you want to validate function arguments using Sah
291             schemas. See L<Data::Sah::Manual::ParamsValidating>.
292              
293             =head1 FUNCTIONS
294              
295              
296             =head2 gen_args_validator
297              
298             Usage:
299              
300             gen_args_validator(%args) -> any
301              
302             Generate argument validator from Rinci function metadata.
303              
304             This function is not exported by default, but exportable.
305              
306             Arguments ('*' denotes required arguments):
307              
308             =over 4
309              
310             =item * B<die> => I<bool>
311              
312             Whether validator should die or just return an error message/response.
313              
314             =item * B<meta> => I<hash>
315              
316             If not specified, will be searched from caller's C<%SPEC> package variable.
317              
318             =item * B<source> => I<bool>
319              
320             Whether we want to get the source code instead.
321              
322             The default is to generate Perl validator code, compile it with C<eval()>, and
323             return the resulting coderef. When this option is set to true, the generated
324             source string will be returned instead.
325              
326             =back
327              
328             Return value: (any)
329              
330             =head1 HOMEPAGE
331              
332             Please visit the project's homepage at L<https://metacpan.org/release/Perinci-Sub-ValidateArgs>.
333              
334             =head1 SOURCE
335              
336             Source repository is at L<https://github.com/perlancar/perl-Perinci-Sub-ValidateArgs>.
337              
338             =head1 BUGS
339              
340             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Perinci-Sub-ValidateArgs>
341              
342             When submitting a bug or request, please include a test-file or a
343             patch to an existing test-file that illustrates the bug or desired
344             feature.
345              
346             =head1 SEE ALSO
347              
348             L<Rinci>, L<Data::Sah>
349              
350             L<Dist::Zilla::Plugin::IfBuilt>
351              
352             L<Dist::Zilla::Plugin::Rinci::Validate>
353              
354             =head1 AUTHOR
355              
356             perlancar <perlancar@cpan.org>
357              
358             =head1 COPYRIGHT AND LICENSE
359              
360             This software is copyright (c) 2019, 2016 by perlancar@cpan.org.
361              
362             This is free software; you can redistribute it and/or modify it under
363             the same terms as the Perl 5 programming language system itself.
364              
365             =cut