File Coverage

blib/lib/Params/Sah.pm
Criterion Covered Total %
statement 103 105 98.1
branch 35 44 79.5
condition 5 8 62.5
subroutine 12 12 100.0
pod 1 1 100.0
total 156 170 91.7


line stmt bran cond sub pod time code
1             package Params::Sah;
2              
3             our $DATE = '2015-03-23'; # DATE
4             our $VERSION = '0.03'; # VERSION
5              
6 1     1   421 use 5.010001;
  1         2  
  1         24  
7 1     1   3 use strict;
  1         0  
  1         17  
8 1     1   3 use warnings;
  1         0  
  1         14  
9              
10 1     1   2 use Carp;
  1         1  
  1         54  
11 1     1   369 use Data::Dmp qw(dmp);
  1         935  
  1         39  
12              
13 1     1   4 use Exporter qw(import);
  1         0  
  1         739  
14             our @EXPORT_OK = qw(gen_validator);
15              
16             our $DEBUG;
17              
18             sub gen_validator {
19 5     5 1 4894 require Data::Sah;
20              
21 5         4284 state $sah = Data::Sah->new;
22 5         32 state $plc = $sah->get_compiler('perl');
23              
24 5         15704 my $opts;
25 5 100       12 if (ref($_[0]) eq 'HASH') {
26 3         3 $opts = shift;
27             } else {
28 2         4 $opts = {};
29             }
30 5   100     17 $opts->{on_invalid} //= 'croak';
31 5 50       26 croak "Invalid on_invalid value, must be: croak|carp|warn|die|bool|str"
32             unless $opts->{on_invalid} =~ /\A(croak|carp|warn|die|bool|str)\z/;
33              
34 5         4 my %schemas;
35 5 100       8 if ($opts->{named}) {
36 3         7 %schemas = @_;
37 3         7 for (keys %schemas) {
38 6 50       17 croak "Invalid argument name, must be alphanums only"
39             unless /\A[A-Za-z_]\w*\z/;
40             }
41             } else {
42 2         2 my $i = 0;
43 2         4 %schemas = map {$i++ => $_} @_;
  4         10  
44             }
45              
46 5         7 my $src = '';
47              
48 5         5 my $i = 0;
49 5         5 my %mentioned_mods;
50             my %mentioned_vars;
51              
52             # currently prototype won't force checking
53 5 100       6 if ($opts->{named}) {
54 3         4 $src .= "sub(\\%) {\n";
55             } else {
56 2         4 $src .= "sub(\\@) {\n";
57             }
58              
59 5         5 $src .= " my \$_ps_args = shift;\n";
60 5 100       12 $src .= " my \$_ps_res;\n" unless $opts->{on_invalid} eq 'bool';
61              
62 5         13 for my $argname (sort keys %schemas) {
63 10         14 $src .= "\n\n ### validating $argname:\n";
64 10         8 my ($argterm, $data_name);
65 10 100       13 if ($opts->{named}) {
66 6         10 $argterm = '$_ps_args->{'.dmp($argname).'}';
67 6         101 $data_name = $argname;
68             } else {
69 4         7 $argterm = '$_ps_args->['.$argname.']';
70 4         3 $data_name = "arg$argname";
71             }
72 10 100       16 my $return_type = $opts->{on_invalid} eq 'bool' ? 'bool' : 'str';
73 10         35 my $cd = $plc->compile(
74             data_name => $data_name,
75             data_term => $argterm,
76             err_term => '$_ps_res',
77             schema => $schemas{$argname},
78             return_type => $return_type,
79             indent_level => 1,
80             );
81 10         59359 for my $mod (sort keys %{ $cd->{module_statements} }) {
  10         34  
82 10 100       27 next if $mentioned_mods{$mod}++;
83 5         7 my $stmt = $cd->{module_statements}{$mod};
84             $src .= " $stmt->[0] $mod".
85 5         17 ($stmt->[1] && @{ $stmt->[1] } ?
86 5 50 33     13 " ".join(", ", @{ $stmt->[1] }) : "").";\n";
87             }
88 10         9 for my $mod (@{ $cd->{modules} }) {
  10         26  
89 15 100       31 next if $cd->{module_statements}{$mod};
90 5         16 (my $mod_pm = $mod) =~ s!::!/!g; $mod_pm .= ".pm";
  5         5  
91 5 50       9 next if $mentioned_mods{$mod}++;
92 5         390 require $mod_pm;
93             }
94 10         445 for my $var (sort keys %{$cd->{vars}}) {
  10         21  
95 8 100       18 next if $mentioned_vars{$var}++;
96 4         5 my $val = $cd->{vars}{$var};
97 4 50       18 $src .= " my \$$var" . (defined($val) ? " = ".dmp($val) : "").
98             ";\n";
99             }
100 10 100       154 if ($opts->{on_invalid} =~ /\A(croak|carp|warn|die)\z/) {
101 6 50       22 my $stmt = $opts->{on_invalid} =~ /\A(croak|carp)\z/ ?
102             "Carp::$opts->{on_invalid}" : $opts->{on_invalid};
103 6 50 66     22 $src .= " undef \$_ps_res;\n" if
104             $i && $opts->{on_invalid} =~ /\A(carp|warn)\z/;
105 6         17 $src .= " $stmt(\"$data_name: \$_ps_res\") ".
106             "if !($cd->{result});\n";
107             } else {
108 4 100       7 if ($return_type eq 'str') {
109 2         6 $src .= " return \"$data_name: \$_ps_res\" ".
110             "if !($cd->{result});\n";
111             } else {
112 2         4 $src .= " return 0 if !($cd->{result});\n";
113             }
114             }
115 10         190 $i++;
116             } # for $argname
117              
118 5 100       16 if ($opts->{on_invalid} eq 'bool') {
    100          
119 1         4 $src .= " return 1\n";
120             } elsif ($opts->{on_invalid} eq 'str') {
121 1         1 $src .= " return '';\n";
122             } else {
123 3         2 $src .= " return;\n";
124             }
125              
126 5         6 $src .= "\n};";
127 5 50       8 if ($DEBUG) {
128 0         0 require String::LineNumber;
129 0         0 say "DEBUG: Validator code:\n" . String::LineNumber::linenum($src);
130             }
131              
132 1     1   3 my $code = eval $src;
  1     1   1  
  1     1   120  
  1     1   3  
  1     1   1  
  1         125  
  1         4  
  1         1  
  1         124  
  1         3  
  1         1  
  1         65  
  1         4  
  1         0  
  1         114  
  5         312  
133 5 50       11 $@ and die
134             "BUG: Can't compile validator code: $@\nValidator code: $code\n";
135 5         19 $code;
136             }
137              
138             1;
139             # ABSTRACT: Validate method/function parameters using Sah schemas
140              
141             __END__
142              
143             =pod
144              
145             =encoding UTF-8
146              
147             =head1 NAME
148              
149             Params::Sah - Validate method/function parameters using Sah schemas
150              
151             =head1 VERSION
152              
153             This document describes version 0.03 of Params::Sah (from Perl distribution Params-Sah), released on 2015-03-23.
154              
155             =head1 SYNOPSIS
156              
157             use Params::Sah qw(gen_validator);
158              
159             # for subroutines that accept positional parameters
160             sub mysub1 {
161             state $validator = gen_validator('str*', 'int');
162             $validator->(\@_);
163             }
164              
165             # for subroutines that accept named parameters
166             sub mysub2 {
167             my %args = @_;
168              
169             state $validator = gen_validator({named=>1}, name=>'str*', age=>'int');
170             $validator->(\%args);
171             }
172              
173             Examples for more complex schemas:
174              
175             gen_validator(
176             {named => 1},
177             name => ['str*', min_len=>4, match=>qr/\S/],
178             age => ['int', min=>17, max=>120],
179             );
180              
181             Validator generation options:
182              
183             # default is to 'croak', valid values include: carp, die, warn, bool, str
184             gen_validator({on_invalid=>'croak'}, ...);
185              
186             =head1 DESCRIPTION
187              
188             This module provides a way for functions to validate their parameters using
189             L<Sah> schemas.
190              
191             The interface is rather different than L<Params::Validate> because it returns a
192             validator I<code> instead of directly validating parameters. The returned
193             validator code is the actual routine that performs parameters checking. This is
194             done for performance reason. For efficiency, you need to cache this validator
195             code instead of producing them at each function call, thus the use of C<state>
196             variables.
197              
198             Performance is faster than Params::Validate, since you can avoid recompiling
199             specification or copying array/hash twice. Sah also provides a rich way to
200             validate data structures.
201              
202             =head1 FUNCTIONS
203              
204             None exported by default, but exportable.
205              
206             =head2 gen_validator([\%opts, ] ...) => code
207              
208             Generate code for subroutine validation. It accepts an optional hashref as the
209             first argument for options. The rest of the arguments are Sah schemas that
210             corresponds to the function parameter in the same position, i.e. the first
211             schema will validate the function's first argument, and so on. Example:
212              
213             gen_validator('schema1', 'schema2', ...);
214             gen_validator({option=>'val', ...}, 'schema1', 'schema2', ...);
215              
216             Will return a coderef which is the validator code. The code accepts a hashref
217             (usually C<< \@_ >>).
218              
219             Known options:
220              
221             =over
222              
223             =item * named => bool (default: 0)
224              
225             If set to true, it means we are generating validator for subroutine that accepts
226             named parameters (e.g. C<< f(name=>'val', other=>'val2') >>) instead of
227             positional (e.g. C<< f('val', 'val2') >>). The validator will accept the
228             parameters as a hashref. And the arguments of C<gen_validator> are assumed to be
229             a hash of parameter names and schemas instead of a list of schemas, for example:
230              
231             gen_validator({named=>1}, arg1=>'schema1', arg2=>'schema2', ...);
232              
233             =item * on_invalid => str (default: 'croak')
234              
235             What should the validator code do when function parameters are invalid? The
236             default is to croak (see L<Carp>) to report error to STDERR from the caller
237             perspective. Other valid choices include: C<warn>, C<carp>, C<die>, C<bool>
238             (return false on invalid, or true on valid), C<str> (return an error message on
239             invalid, or empty string on valid).
240              
241             =back
242              
243             =head1 PERFORMANCE NOTES
244              
245             Sample benchmark against Params::Validate:
246              
247             #!/usr/bin/env perl
248            
249             use 5.010;
250             use strict;
251             use warnings;
252            
253             use Benchmark::Dumb qw(cmpthese);
254             use Params::Sah qw(gen_validator);
255             use Params::Validate qw(:all);
256            
257             my @data1_pos = ("ujang");
258            
259             my @data2_named = (name=>"ujang", age=>30);
260             my @data2_pos = ("ujang", 30);
261            
262             cmpthese(0, {
263             'P::Sah, pos, str' => sub {
264             state $validator = gen_validator(
265             'str*',
266             );
267             $validator->(\@data1_pos);
268             },
269             'P::V, pos, str' => sub {
270             validate_pos(@data1_pos,
271             {type=>SCALAR},
272             );
273             },
274            
275             'P::Sah, pos, str+int' => sub {
276             state $validator = gen_validator(
277             'str*',
278             'int',
279             );
280             $validator->(\@data2_pos);
281             },
282             'P::V, pos, str+int' => sub {
283             validate_pos(@data2_pos,
284             {type=>SCALAR},
285             {type=>SCALAR, regex=>qr/\A\d+\z/, optional=>1},
286             );
287             },
288             'P::Sah, named, str+int' => sub {
289             state $validator = gen_validator(
290             {named=>1},
291             name => 'str*',
292             age => 'int',
293             );
294             $validator->({@data2_named});
295             },
296             'P::V, named, str+int' => sub {
297             validate(@data2_named, {
298             name => {type=>SCALAR},
299             age => {type=>SCALAR, regex=>qr/\A\d+\z/, optional=>1},
300             });
301             },
302             });
303            
304             # XXX test positional
305              
306             Sample benchmark result on my laptop:
307              
308             Rate P::V, named, str+int P::V, pos, str+int P::V, pos, str P::Sah, named, str+int P::Sah, pos, str+int P::Sah, pos, str
309             P::V, named, str+int 77993.2+-0.14/s -- -28.3% -72.3% -83.0% -90.7% -92.9%
310             P::V, pos, str+int 108710+-140/s 39.38+-0.18% -- -61.4% -76.3% -87.0% -90.1%
311             P::V, pos, str 281590+-530/s 261.04+-0.68% 159.03+-0.59% -- -38.6% -66.4% -74.4%
312             P::Sah, named, str+int 458440+-180/s 487.8% 321.71+-0.57% 62.81+-0.31% -- -45.2% -58.3%
313             P::Sah, pos, str+int 837250+-880/s 973.5+-1.1% 670.2+-1.3% 197.33+-0.64% 82.63+-0.2% -- -23.9%
314             P::Sah, pos, str 1.0997e+06+-24/s 1310.0% 911.6+-1.3% 290.54+-0.73% 139.9% 31.35+-0.14% --
315              
316             =head1 FAQ
317              
318             =head2 Why does the validator code accept arrayref/hashref instead of array/hash?
319              
320             To be able to modify the original array/hash, e.g. set default value.
321              
322             =head2 How to give default value to parameters?
323              
324             By using the Sah C<default> clause:
325              
326             gen_validator(['str*', default=>'green']);
327              
328             =head2 Why is my program failing with error message: Can't call method "state" on an undefined value?
329              
330             You need to use Perl 5.10 or newer and enable the 5.10 feature 'state':
331              
332             use 5.010;
333              
334             or:
335              
336             use feature 'state';
337              
338             =head2 How do I see the validator code being generated?
339              
340             Set C<$Params::Sah::DEBUG=1> before C<gen_validator()>, for example:
341              
342             use Params::Sah qw(gen_validator);
343              
344             $Params::Sah::DEBUG = 1;
345             gen_validator('int*', 'str');
346              
347             Sample output:
348              
349             1|sub(\@) {
350             2| my $_ps_args = shift;
351             3| my $_ps_res;
352             |
353             |
354             6| ### validating 0:
355             7| no warnings 'void';
356             8| my $_sahv_dpath = [];
357             9| Carp::croak("arg0: $_ps_res") if !( # req #0
358             10| ((defined($_ps_args->[0])) ? 1 : (($_ps_res //= (@$_sahv_dpath ? '@'.join("/",@$_sahv_dpath).": " : "") . "Required but not specified"),0))
359             |
360             12| &&
361             |
362             14| # check type 'int'
363             15| ((Scalar::Util::Numeric::isint($_ps_args->[0])) ? 1 : (($_ps_res //= (@$_sahv_dpath ? '@'.join("/",@$_sahv_dpath).": " : "") . "Not of type integer"),0)));
364             |
365             |
366             18| ### validating 1:
367             19| Carp::croak("arg1: $_ps_res") if !( # skip if undef
368             20| (!defined($_ps_args->[1]) ? 1 :
369             |
370             22| (# check type 'str'
371             23| ((!ref($_ps_args->[1])) ? 1 : (($_ps_res //= (@$_sahv_dpath ? '@'.join("/",@$_sahv_dpath).": " : "") . "Not of type text"),0)))));
372             24| return;
373             |
374             26|};
375              
376             =head1 SEE ALSO
377              
378             L<Sah>, L<Data::Sah>
379              
380             L<Params::Validate>
381              
382             L<Perinci::Sub::Wrapper>, if you want to do more than parameter validation.
383              
384             =head1 HOMEPAGE
385              
386             Please visit the project's homepage at L<https://metacpan.org/release/Params-Sah>.
387              
388             =head1 SOURCE
389              
390             Source repository is at L<https://github.com/perlancar/perl-Params-Sah>.
391              
392             =head1 BUGS
393              
394             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Params-Sah>
395              
396             When submitting a bug or request, please include a test-file or a
397             patch to an existing test-file that illustrates the bug or desired
398             feature.
399              
400             =head1 AUTHOR
401              
402             perlancar <perlancar@cpan.org>
403              
404             =head1 COPYRIGHT AND LICENSE
405              
406             This software is copyright (c) 2015 by perlancar@cpan.org.
407              
408             This is free software; you can redistribute it and/or modify it under
409             the same terms as the Perl 5 programming language system itself.
410              
411             =cut