File Coverage

blib/lib/Params/Sah.pm
Criterion Covered Total %
statement 149 155 96.1
branch 43 54 79.6
condition 26 34 76.4
subroutine 22 22 100.0
pod 1 1 100.0
total 241 266 90.6


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