File Coverage

blib/lib/Params/Sah.pm
Criterion Covered Total %
statement 156 164 95.1
branch 44 62 70.9
condition 35 50 70.0
subroutine 23 23 100.0
pod 1 1 100.0
total 259 300 86.3


line stmt bran cond sub pod time code
1             package Params::Sah;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2020-05-10'; # DATE
5             our $DIST = 'Params-Sah'; # DIST
6             our $VERSION = '0.072'; # VERSION
7              
8 1     1   109584 use 5.010001;
  1         8  
9 1     1   6 use strict;
  1         2  
  1         20  
10 1     1   5 use warnings;
  1         2  
  1         43  
11 1     1   1762 use Log::ger;
  1         50  
  1         6  
12              
13 1     1   257 use Exporter qw(import);
  1         2  
  1         1614  
14             our @EXPORT_OK = qw(gen_validator);
15              
16             our $OPT_BACKEND = 'Data::Sah';
17             our $OPT_ON_INVALID = 'croak';
18             our $OPT_INVALID_DETAIL = 0;
19             our $OPT_NAMED = 0;
20             our $OPT_DISABLE = 0;
21             our $OPT_ALLOW_EXTRA = 0;
22             our $DEBUG;
23              
24             sub _plc {
25 26     26   1303 require Data::Sah;
26 26         5525 state $sah = Data::Sah->new;
27 26         73 state $plc = $sah->get_compiler('perl');
28 26         32686 $plc;
29             }
30              
31             sub gen_validator {
32 19     19 1 69455 my ($opt_backend,
33             $opt_on_invalid, $opt_named, $opt_disable, $opt_allow_extra,
34             $opt_invalid_detail, $opt_optional_params);
35             {
36 19         36 my $opts;
  19         29  
37 19 100       64 if (ref($_[0]) eq 'HASH') {
38 14         19 $opts = {%{shift()}};
  14         45  
39             } else {
40 5         11 $opts = {};
41             }
42              
43 19   66     108 $opt_backend = delete $opts->{backend} // $OPT_BACKEND // 'Data::Sah';
      50        
44 19 50       111 die "Invalid backend value, must be: Data::Sah|Data::Sah::Tiny"
45             unless $opt_backend =~ /\A(Data::Sah::Tiny|Data::Sah)\z/;
46 19   66     73 $opt_on_invalid = delete $opts->{on_invalid} // $OPT_ON_INVALID //
      50        
47             'croak';
48 19 100       92 die "Invalid on_invalid value, must be: croak|carp|warn|die|return"
49             unless $opt_on_invalid =~ /\A(croak|carp|warn|die|return)\z/;
50              
51 18   33     75 $opt_invalid_detail = delete $opts->{invalid_detail} // $OPT_INVALID_DETAIL // 0;
      50        
52 18 50 66     51 die "Data::Sah::Tiny does not support invalid_detail=>1"
53             if $opt_backend eq 'Data::Sah::Tiny' && $opt_invalid_detail;
54 18   100     53 $opt_named = delete $opts->{named} // $OPT_NAMED // 0;
      50        
55 18   100     58 $opt_disable = delete $opts->{disable} // $OPT_DISABLE // 0;
      50        
56 18   100     56 $opt_allow_extra = delete $opts->{allow_extra} // $OPT_ALLOW_EXTRA // 0;
      50        
57 18   100     51 $opt_optional_params = delete $opts->{optional_params} // [];
58 18 100       94 keys(%$opts) and die "Uknown gen_validator() option(s) specified: ".
59             join(", ", sort keys %$opts);
60             }
61 17 100       39 if ($opt_disable) {
62 2 50   2   16 return $opt_on_invalid eq 'str' ? sub {''} : sub {1};
  0         0  
  2         89  
63             }
64              
65 15         81 require Carp;
66 15         639 require Data::Dmp;
67              
68 15         2000 my %schemas;
69             my @schema_keys;
70 15 100       37 if ($opt_named) {
71 7         45 %schemas = @_;
72 7         32 @schema_keys = sort keys %schemas;
73 7         15 for (@schema_keys) {
74 14 50       58 Carp::croak("Invalid argument name, must be alphanums only")
75             unless /\A[A-Za-z_][A-Za-z0-9_]*\z/;
76             }
77             } else {
78 8         11 my $i = 0;
79 8         19 %schemas = map {$i++ => $_} @_;
  13         47  
80 8         30 @schema_keys = reverse 0..$i-1;
81             }
82              
83 15         31 my $src = '';
84              
85 15         19 my $i = 0;
86 15         24 my @modules_for_all_args;
87             my %mentioned_vars;
88              
89             my $code_get_err_stmt = sub {
90 60     60   1586 my ($err_term_detail, $err_term_generic) = @_;
91 60 100       221 if ($opt_on_invalid =~ /\A(croak|carp|warn|die)\z/) {
92 52 50       163 my $stmt = $opt_on_invalid =~ /\A(croak|carp)\z/ ?
93             "Carp::$opt_on_invalid" : $opt_on_invalid;
94 52 50 66     302 return "$stmt(".($opt_invalid_detail ? $err_term_detail : $err_term_generic // $err_term_detail).")";
95             } else {
96             # return
97 8 50       15 if ($opt_invalid_detail) {
98 0         0 return "return $err_term_detail";
99             } else {
100 8         24 return "return 0";
101             }
102             }
103 15         68 };
104              
105             # currently prototype won't force checking
106             #if ($opt_named) {
107             # $src .= "sub(\\%) {\n";
108             #} else {
109             # $src .= "sub(\\@) {\n";
110             #}
111 15         31 $src .= "sub {\n";
112              
113 15         26 $src .= " my \$_ps_args = shift;\n";
114 15 50       31 $src .= " my \$_ps_res;\n" if $opt_invalid_detail;
115              
116 15 100       30 unless ($opt_allow_extra) {
117 11         18 $src .= "\n ### checking unknown arguments\n";
118 11 100       23 if ($opt_named) {
119 5         13 $src .= " state \$_ps_known_args = ".Data::Dmp::dmp({map {$_=>1} @schema_keys}).";\n";
  10         31  
120 5         457 $src .= " my \@_ps_unknown_args;\n";
121 5         8 $src .= " for (keys %\$_ps_args) { push \@_ps_unknown_args, \$_ unless exists \$_ps_known_args->{\$_} }\n";
122 5         13 $src .= " if (\@_ps_unknown_args) { ".$code_get_err_stmt->(qq("There are extra unknown parameter(s): ".join(", ", \@_ps_unknown_args)))." }\n";
123             } else {
124 6         17 $src .= " if (\@\$_ps_args > ".(scalar keys %schemas).") {\n";
125 6         15 $src .= " ".$code_get_err_stmt->(qq("There are extra additional parameter(s)")).";\n";
126 6         12 $src .= " }\n";
127             }
128             }
129              
130 15         35 for my $argname (@schema_keys) {
131 27 100       65 unless (grep { $argname eq $_ } @$opt_optional_params) {
  10         37  
132 22         47 $src .= "\n ### checking $argname exists:\n";
133 22 100       42 if ($opt_named) {
134 11         29 $src .= "\n unless (exists \$_ps_args->{".Data::Dmp::dmp($argname)."}) { ".$code_get_err_stmt->(qq("Missing required parameter '$argname'"))." }\n";
135             } else {
136 11         32 $src .= "\n if (\@\$_ps_args <= $argname) { ".$code_get_err_stmt->(qq("Missing required parameter [$argname]"))." }\n";
137             }
138             }
139              
140 27         62 $src .= "\n ### validating $argname:\n";
141 27         43 my ($argterm, $data_name);
142 27 100       46 if ($opt_named) {
143 14         30 $argterm = '$_ps_args->{'.Data::Dmp::dmp($argname).'}';
144 14         397 $data_name = $argname;
145             } else {
146 13         27 $argterm = '$_ps_args->['.$argname.']';
147 13         18 $data_name = "arg$argname";
148             }
149 27         35 my $cd;
150 27 100       45 if ($opt_backend eq 'Data::Sah') {
151             $cd = _plc->compile(
152             data_name => $data_name,
153             data_term => $argterm,
154             err_term => '$_ps_res',
155 26 50       57 schema => $schemas{$argname},
156             return_type => $opt_invalid_detail ? 'str' : 'bool',
157             indent_level => 1,
158             );
159             } else {
160 1         6 require Data::Sah::Tiny;
161 1         6 $cd = Data::Sah::Tiny::gen_validator($schemas{$argname}, {
162             hash => 1,
163             data_term => $argterm,
164             });
165             }
166 27 50       153519 die "Incompatible Data::Sah version (cd v=$cd->{v}, expected 2)" unless $cd->{v} == 2;
167 27         46 for my $mod_rec (@{ $cd->{modules} }) {
  27         65  
168 72 100       156 next unless $mod_rec->{phase} eq 'runtime';
169 45 100       89 next if grep { ($mod_rec->{use_statement} && $_->{use_statement} && $_->{use_statement} eq $mod_rec->{use_statement}) ||
170 49 100 100     302 $_->{name} eq $mod_rec->{name} } @modules_for_all_args;
      66        
171 27         39 push @modules_for_all_args, $mod_rec;
172 27   66     99 $src .= " ".($mod_rec->{use_statement} // "require $mod_rec->{name}").";\n";
173             }
174 27         43 for my $var (sort keys %{$cd->{vars}}) {
  27         121  
175 0 0       0 next if $mentioned_vars{$var}++;
176 0         0 my $val = $cd->{vars}{$var};
177 0 0       0 $src .= " my \$$var" . (defined($val) ? " = ".Data::Dmp::dmp($val) : "").
178             ";\n";
179             }
180 27 50 66     100 $src .= " undef \$_ps_res;\n" if
181             $i && $opt_on_invalid =~ /\A(carp|warn)\z/;
182 27         121 $src .= " ".$code_get_err_stmt->(qq("$data_name: \$_ps_res"), qq("$data_name: fail schema ".).Data::Dmp::dmp($schemas{$argname}))." if !($cd->{result});\n";
183 27         548 $i++;
184             } # for $argname
185              
186 15 50       40 if ($opt_invalid_detail) {
187 0         0 $src .= "\n return '';\n";
188             } else {
189 15         26 $src .= "\n return 1;\n";
190             }
191              
192 15         24 $src .= "\n};";
193 15 50       43 if ($DEBUG) {
194 0         0 require String::LineNumber;
195 0         0 say "DEBUG: Validator code:\n" . String::LineNumber::linenum($src);
196             }
197              
198 1     1   9 my $code = eval $src;
  1     1   2  
  1     1   110  
  1     1   7  
  1     1   3  
  1     1   141  
  1     1   8  
  1     1   2  
  1     1   156  
  1     1   7  
  1     1   3  
  1     1   143  
  1     1   7  
  1     1   2  
  1         216  
  1         7  
  1         3  
  1         165  
  1         7  
  1         3  
  1         143  
  1         10  
  1         2  
  1         147  
  1         8  
  1         1  
  1         147  
  1         9  
  1         2  
  1         144  
  1         8  
  1         2  
  1         163  
  1         8  
  1         2  
  1         155  
  1         7  
  1         3  
  1         139  
  1         9  
  1         2  
  1         132  
  15         2021  
199 15 50       54 $@ and die
200             "BUG: Can't compile validator code: $@\nValidator code: $code\n";
201 15         189 $code;
202             }
203              
204             1;
205             # ABSTRACT: Validate method/function parameters using Sah schemas
206              
207             __END__
208              
209             =pod
210              
211             =encoding UTF-8
212              
213             =head1 NAME
214              
215             Params::Sah - Validate method/function parameters using Sah schemas
216              
217             =head1 VERSION
218              
219             This document describes version 0.072 of Params::Sah (from Perl distribution Params-Sah), released on 2020-05-10.
220              
221             =head1 SYNOPSIS
222              
223             use Params::Sah qw(gen_validator);
224              
225             # for subroutines that accept positional parameters. all parameters required,
226             # but you can pass undef to the third param.
227             sub mysub1 {
228             state $validator = gen_validator('str*', ['array*', min_len=>1], 'int');
229             $validator->(\@_);
230             ...
231             }
232             mysub1("john", ['a']); # dies, the third argument is not passed
233             mysub1("john", ['a'], 2); # ok
234             mysub1("john", ['a'], 2, 3); # dies, extra parameter
235             mysub1("john", ['a'], undef); # ok, even though the third argument is undef
236             mysub1([], ['a'], undef); # dies, first argument does not validate
237             mysub1("john", [], undef); # dies, second argument does not validate
238              
239             # for subroutines that accept positional parameters (this time arrayref instead
240             # of array), some parameters optional. also this time we use 'allow_extra'
241             # option to allow additional positional parameters.
242             sub mysub1b {
243             my $args = shift;
244             state $validator = gen_validator({optional_params=>[2], allow_extra=>1}, 'str*', 'array*', 'int');
245             $validator->($args);
246             ...
247             }
248             mysub1b(["john", ['a']]); # ok, the third argument is optional
249             mysub1b(["john", ['a'], 2]); # ok
250             mysub1b(["john", ['a'], undef]); # ok
251             mysub1b(["john", ['a'], 2, 3]); # ok, extra params allowed
252              
253             # for subroutines that accept named parameters (as hash). all parameters
254             # required, but you can pass undef to the 'age' parameter.
255             sub mysub2 {
256             my %args = @_;
257              
258             state $validator = gen_validator({named=>1}, name=>'str*', tags=>['array*', min_len=>1], age=>'int');
259             $validator->(\%args);
260             ...
261             }
262             mysub2(name=>"john", tags=>['a']); # dies, the 'age' argument is not passed
263             mysub2(name=>"john", tags=>['a'], age=>32); # ok
264             mysub2(name=>"john", tags=>['a'], age=>undef); # ok, even though the 'age' argument is undef
265             mysub2(name=>[], tags=>['a'], age=>undef); # dies, the 'name' argument does not validate
266             mysub2(name=>"john", tags=>[], age=>undef); # dies, the 'tags' argument does not validate
267              
268             # for subroutines that accept named parameters (this time as hashref). some
269             # parameters optional. also this time we want to allow extra named parameters.
270             sub mysub2b {
271             my $args = shift;
272              
273             state $validator = gen_validator(
274             {named=>1, optional_params=>['age'], allow_extra=>1},
275             name=>'str*',
276             tags=>['array*', min_len=>1],
277             age=>'int*',
278             );
279             $validator->($args);
280             ...
281             }
282             mysub2b({name=>"john", tags=>['a']}); # ok
283             mysub2b({name=>"john", tags=>['a'], age=>32}); # ok
284             mysub2b({name=>"john", tags=>['a'], age=>32, foo=>1}); # ok, extra param 'foo' allowed
285             mysub2b({name=>"john", tags=>['a'], age=>undef}); # dies, this time, 'age' cannot be undef
286              
287             Example with more complex schemas, with default value and coercion rules:
288              
289             sub mysub2c {
290             my %args = @_;
291             state $validator = gen_validator(
292             {named => 1, optional_params => ['age']},
293             name => ['str*', min_len=>4, match=>qr/\S/, default=>'noname'],
294             age => ['int', min=>17, max=>120],
295             tags => ['array*', min_len=>1, of=>['str*', match=>qr/\A\w+\z/], 'x.perl.coerce_rules'=>['From_str::comma_sep']],
296             );
297             $validator->(\%args);
298             ...
299             }
300             mysub2c(tags=>['a']); # after validation, %args will be: (name=>'noname', tags=>['a'])
301             mysub2c(name=>"mark", tags=>['b,c,d']); # after validation, %args will be: (name=>'mark', tags=>['b','c','d'])
302              
303             Validator generation options:
304              
305             # default is to 'croak', valid values include: carp, die, warn, bool, str
306             gen_validator({on_invalid=>'croak'}, ...);
307              
308             =head1 DESCRIPTION
309              
310             This module provides a way for functions to validate their parameters using
311             L<Sah> schemas.
312              
313             =head1 VARIABLES
314              
315             =head2 $DEBUG
316              
317             Bool. If set to true will print validator code when generated.
318              
319             =head2 $OPT_BACKEND
320              
321             Str. Used to set default for C<backend> option.
322              
323             =head2 $OPT_ALLOW_EXTRA
324              
325             Bool. Used to set default for C<allow_extra> option.
326              
327             =head2 $OPT_ON_INVALID
328              
329             String. Used to set default for C<on_invalid> option.
330              
331             =head2 $OPT_ERR_DETAIL
332              
333             Bool. Used to set default for C<err_detail> option.
334              
335             =head2 $OPT_DISABLE
336              
337             Bool. Used to set default for C<disable> option.
338              
339             =head2 $OPT_NAMED
340              
341             Bool. Used to set default for C<named> option.
342              
343             =head1 PERFORMANCE NOTES
344              
345             See benchmarks in L<Bencher::Scenarios::ParamsSah>.
346              
347             =head1 FUNCTIONS
348              
349             None exported by default, but exportable.
350              
351             =head2 gen_validator([\%opts, ] ...) => code
352              
353             Generate code for subroutine validation. It accepts an optional hashref as the
354             first argument for options. The rest of the arguments are Sah schemas that
355             correspond to the function parameters in the same position, i.e. the first
356             schema will validate the function's first argument, and so on. Example:
357              
358             gen_validator('schema1', 'schema2', ...);
359             gen_validator({option=>'val', ...}, 'schema1', 'schema2', ...);
360              
361             Will return a coderef which is the validator code. The code accepts an arrayref
362             (usually C<< \@_ >>).
363              
364             Known options:
365              
366             =over
367              
368             =item * backend => str (default: Data::Sah)
369              
370             Can be set to the experimental L<Data::Sah::Tiny> to speed up validator
371             generation for simpler schemas.
372              
373             =item * named => bool (default: 0)
374              
375             If set to true, it means we are generating validator for subroutine that accepts
376             named parameters (e.g. C<< f(name=>'val', other=>'val2') >>) instead of
377             positional (e.g. C<< f('val', 'val2') >>). The validator will accept the
378             parameters as a hashref. And the arguments of C<gen_validator> are assumed to be
379             a hash of parameter names and schemas instead of a list of schemas, for example:
380              
381             gen_validator({named=>1}, arg1=>'schema1', arg2=>'schema2', ...);
382              
383             =item * optional_params => array
384              
385             By default all parameters are required. This option specifies which parameters
386             should be made optional. For positional parameters, specify the index (0-based).
387              
388             =item * allow_extra => bool (default: 0)
389              
390             If set to one then additional positional or named parameters are allowed (and
391             not validated). By default, no extra parameters are allowed.
392              
393             =item * on_invalid => str (default: 'croak')
394              
395             What should the validator code do when function parameters are invalid? The
396             default is to croak (see L<Carp>) to report error to STDERR from the caller
397             perspective. Other valid choices include: C<warn>, C<carp>, C<die>, C<bool>
398             (return false on invalid, or true on valid), C<str> (return an error message on
399             invalid, or empty string on valid).
400              
401             =item * invalid_detail => bool (default: 0)
402              
403             If set to true, will generate a more detailed error message. For example, with
404             this schema:
405              
406             [str => {min_len=>4}]
407              
408             then the string C<'foo'> will fail to validate with this error message "Length
409             must be at least 4". Otherwise, the error message will just be something like:
410             "Fail schema ['str', {min_len=>1}]". By default this option is set to false for
411             slightly faster validation.
412              
413             =item * disable => bool (default: 0)
414              
415             If set to 1, will return an empty coderef validator. Used to disable parameter
416             checking. Usually via setting L</$OPT_DISABLE> to disable globally.
417              
418             =back
419              
420             =head1 FAQ
421              
422             =head2 How do I learn more about Sah (the schema language)?
423              
424             See the specification: L<Sah>. The L<Sah::Examples> distribution also contains
425             more examples. Also, for other examples, lots of my distributions contain
426             L<Rinci> metadata which includes schemas for each function arguments.
427              
428             =head2 Why does the validator code accept arrayref/hashref instead of array/hash?
429              
430             To be able to modify the original array/hash, e.g. set default value.
431              
432             =head2 What if my subroutine accepts a mix of positional and named parameters?
433              
434             You can put all your parameters in a hash first, then feed it to the validator.
435             For example:
436              
437             sub mysub {
438             my %args;
439             %args = %{shift} if req $_[0] eq 'HASH'; # accept optional hashref
440             ($args{x}, $args{y}) = @_; # positional params
441             state $validator = gen_validator(
442             {named=>1, optional_params=>['opt1','opt2']},
443             x=>"posint*",
444             y=>"negint*",
445             opt1=>"str*",
446             opt2=>"str",
447             );
448             $validator->(\%args);
449             ...
450             }
451             mysub(1, -2); # ok, after validation %args will become (x=>1, y=>-2)
452             mysub({}, 1, -2); # ok, after validation %args will become (x=>1, y=>-2)
453             mysub({opt1=>"foo"}, 1, -2); # ok, after validation %args will become (x=>1, y=>-2, opt1=>"foo")
454             mysub({opt3=>"foo"}, 1, -2); # dies, unknown option 'opt3'
455             mysub({opt1=>"foo"}, 1); # dies, missing required arg 'x'
456             mysub({opt1=>[]}, 1, -2); # dies, 'opt1' argument doesn't validate
457              
458             =head2 How to give default value to parameters?
459              
460             By using the Sah C<default> clause in your schema:
461              
462             gen_validator(['str*', default=>'green']);
463              
464             =head2 How to make some parameters optional?
465              
466             By using the C<optional_params> option, which is an array of parameter names to make
467             optional. To set a positional parameter optional, specify its index (0-based) as name.
468              
469             =head2 Why is my program failing with error message: Can't call method "state" on an undefined value?
470              
471             You need to specify that you want to use C<state> variables, either by:
472              
473             # at least
474             use 5.010;
475              
476             or:
477              
478             use feature 'state';
479              
480             =head2 How do I see the validator code being generated?
481              
482             Set C<$Params::Sah::DEBUG=1> before C<gen_validator()>, for example:
483              
484             use Params::Sah qw(gen_validator);
485              
486             $Params::Sah::DEBUG = 1;
487             gen_validator('int*', 'str');
488              
489             Sample output:
490              
491             1|sub(\@) {
492             2| my $_ps_args = shift;
493             3| my $_ps_res;
494             |
495             |
496             6| ### validating 0:
497             7| no warnings 'void';
498             8| my $_sahv_dpath = [];
499             9| Carp::croak("arg0: $_ps_res") if !( # req #0
500             10| ((defined($_ps_args->[0])) ? 1 : (($_ps_res //= (@$_sahv_dpath ? '@'.join("/",@$_sahv_dpath).": " : "") . "Required but not specified"),0))
501             |
502             12| &&
503             |
504             14| # check type 'int'
505             15| ((Scalar::Util::Numeric::isint($_ps_args->[0])) ? 1 : (($_ps_res //= (@$_sahv_dpath ? '@'.join("/",@$_sahv_dpath).": " : "") . "Not of type integer"),0)));
506             |
507             |
508             18| ### validating 1:
509             19| Carp::croak("arg1: $_ps_res") if !( # skip if undef
510             20| (!defined($_ps_args->[1]) ? 1 :
511             |
512             22| (# check type 'str'
513             23| ((!ref($_ps_args->[1])) ? 1 : (($_ps_res //= (@$_sahv_dpath ? '@'.join("/",@$_sahv_dpath).": " : "") . "Not of type text"),0)))));
514             24| return;
515             |
516             26|};
517              
518             =head1 HOMEPAGE
519              
520             Please visit the project's homepage at L<https://metacpan.org/release/Params-Sah>.
521              
522             =head1 SOURCE
523              
524             Source repository is at L<https://github.com/perlancar/perl-Params-Sah>.
525              
526             =head1 BUGS
527              
528             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Params-Sah>
529              
530             When submitting a bug or request, please include a test-file or a
531             patch to an existing test-file that illustrates the bug or desired
532             feature.
533              
534             =head1 SEE ALSO
535              
536             L<Sah>, L<Data::Sah>
537              
538             Alternative modules: L<Params::ValidationCompiler> (a compiled version of
539             L<Params::Validate>), L<Type::Params> (from L<Type::Tiny>).
540              
541             =head1 AUTHOR
542              
543             perlancar <perlancar@cpan.org>
544              
545             =head1 COPYRIGHT AND LICENSE
546              
547             This software is copyright (c) 2020, 2016, 2015 by perlancar@cpan.org.
548              
549             This is free software; you can redistribute it and/or modify it under
550             the same terms as the Perl 5 programming language system itself.
551              
552             =cut