File: | blib/lib/Params/Validate/Strict.pm |
Coverage: | 79.9% |
line | stmt | bran | cond | sub | time | code |
---|---|---|---|---|---|---|
1 | package Params::Validate::Strict; | |||||
2 | ||||||
3 | 9 9 9 | 578727 7 126 | use strict; | |||
4 | 9 9 9 | 18 3 147 | use warnings; | |||
5 | ||||||
6 | 9 9 9 | 14 14 216 | use Carp; | |||
7 | 9 9 9 | 17 77 240 | use List::Util 1.33 qw(any); # Required for memberof validation | |||
8 | 9 9 9 | 15 5 111 | use Exporter qw(import); # Required for @EXPORT_OK | |||
9 | 9 9 9 | 1384 34993 159 | use Params::Get 0.13; | |||
10 | 9 9 9 | 25 4 13236 | use Scalar::Util; | |||
11 | ||||||
12 | our @ISA = qw(Exporter); | |||||
13 | our @EXPORT_OK = qw(validate_strict); | |||||
14 | ||||||
15 - 23 | =head1 NAME Params::Validate::Strict - Validates a set of parameters against a schema =head1 VERSION Version 0.14 =cut | |||||
24 | ||||||
25 | our $VERSION = '0.14'; | |||||
26 | ||||||
27 - 242 | =head1 SYNOPSIS my $schema = { username => { type => 'string', min => 3, max => 50 }, age => { type => 'integer', min => 0, max => 150 }, }; my $input = { username => 'john_doe', age => '30', # Will be coerced to integer }; my $validated_input = validate_strict(schema => $schema, input => $input); if(defined($validated_input)) { print "Example 1: Validation successful!\n"; print 'Username: ', $validated_input->{username}, "\n"; print 'Age: ', $validated_input->{age}, "\n"; # It's an integer now } else { print "Example 1: Validation failed: $@\n"; } =head1 METHODS =head2 validate_strict Validates a set of parameters against a schema. This function takes two mandatory arguments: =over 4 =item * C<schema> A reference to a hash that defines the validation rules for each parameter. The keys of the hash are the parameter names, and the values are either a string representing the parameter type or a reference to a hash containing more detailed rules. =item * C<args> || C<input> A reference to a hash containing the parameters to be validated. The keys of the hash are the parameter names, and the values are the parameter values. =back It takes two optional arguments: =over 4 =item * C<unknown_parameter_handler> This parameter describes what to do when a parameter is given that is not in the schema of valid parameters. It must be one of C<die> (the default), C<warn>, or C<ignore>. =item * C<logger> A logging object that understands messages such as C<error> and C<warn>. =back The schema can define the following rules for each parameter: =over 4 =item * C<type> The data type of the parameter. Valid types are C<string>, C<integer>, C<number>, C<boolean>, C<hashref>, C<arrayref>, C<object> and C<coderef>. =item * C<can> The parameter must be an object that understands the method C<can>. C<can> can be a simple scalar string of a method name, or an arrayref of a list of method names, all of which must be supported by the object. =item * C<isa> The parameter must be an object of type C<isa>. =item * C<memberof> The parameter must be a member of the given arrayref. =item * C<min> The minimum length (for strings), value (for numbers) or number of keys (for hashrefs). =item * C<max> The maximum length (for strings), value (for numbers) or number of keys (for hashrefs). =item * C<matches> A regular expression that the parameter value must match. Checks all members of arrayrefs. =item * C<nomatch> A regular expression that the parameter value must not match. Checks all members of arrayrefs. =item * C<callback> A code reference to a subroutine that performs custom validation logic. The subroutine should accept the parameter value as an argument and return true if the value is valid, false otherwise. =item * C<optional> A boolean value indicating whether the parameter is optional. If true, the parameter is not required. If false or omitted, the parameter is required. =item * C<default> Populate missing optional parameters with the specified value. Note that this value is not validated. username => { type => 'string', optional => 1, default => 'guest' } =item * C<element_type> Extends the validation to individual elements of arrays. tags => { type => 'arrayref', element_type => 'number', min => 1, # this is the length of the array, not the min value for each of the numbers. For that, add a C<schema> rule max => 5 } =item * C<error_message> The custom error message to be used in the event of a validation failure. age => { type => 'integer', min => 18, error_message => 'You must be at least 18 years old' } =item * C<schema> You can validate nested hashrefs and arrayrefs using the C<schema> property: my $schema = { user => { type => 'hashref', schema => { name => { type => 'string' }, age => { type => 'integer', min => 0 }, hobbies => { type => 'arrayref', schema => { type => 'string' }, # Validate each element min => 1 # At least one hobby } } }, metadata => { type => 'hashref', schema => { created => { type => 'string' }, tags => { type => 'arrayref', schema => { type => 'string', matches => qr/^[a-z]+$/ } } } } }; =back If a parameter is optional and its value is C<undef>, validation will be skipped for that parameter. If the validation fails, the function will C<croak> with an error message describing the validation failure. If the validation is successful, the function will return a reference to a new hash containing the validated and (where applicable) coerced parameters. Integer and number parameters will be coerced to their respective types. =head1 MIGRATION FROM LEGACY VALIDATORS =head2 From L<Params::Validate> # Old style validate(@_, { name => { type => SCALAR }, age => { type => SCALAR, regex => qr/^\d+$/ } }); # New style validate_strict( schema => { name => 'string', age => { type => 'integer', min => 0 } }, args => { @_ } ); =head2 From L<Type::Params> # Old style my ($name, $age) = validate_positional \@_, Str, Int; # New style - requires converting to named parameters first my %args = (name => $_[0], age => $_[1]); my $validated = validate_strict( schema => { name => 'string', age => 'integer' }, args => \%args ); =cut | |||||
243 | ||||||
244 | sub validate_strict | |||||
245 | { | |||||
246 | 178 | 775538 | my $params = Params::Get::get_params(undef, \@_); | |||
247 | ||||||
248 | 178 | 2122 | my $schema = $params->{'schema'}; | |||
249 | 178 | 267 | my $args = $params->{'args'} || $params->{'input'}; | |||
250 | 178 | 268 | my $unknown_parameter_handler = $params->{'unknown_parameter_handler'} || 'die'; | |||
251 | 178 | 125 | my $logger = $params->{'logger'}; | |||
252 | ||||||
253 | # Check if schema and args are references to hashes | |||||
254 | 178 | 176 | if(ref($schema) ne 'HASH') { | |||
255 | 2 | 2 | _error($logger, 'validate_strict: schema must be a hash reference'); | |||
256 | } | |||||
257 | ||||||
258 | 176 | 318 | if(exists($params->{'args'}) && (!defined($args))) { | |||
259 | 2 | 2 | $args = {}; | |||
260 | } elsif(ref($args) ne 'HASH') { | |||||
261 | 1 | 1 | _error($logger, 'validate_strict: args must be a hash reference'); | |||
262 | } | |||||
263 | ||||||
264 | 175 175 | 125 191 | foreach my $key (keys %{$args}) { | |||
265 | 240 | 249 | if(!exists($schema->{$key})) { | |||
266 | 11 | 15 | if($unknown_parameter_handler eq 'die') { | |||
267 | 5 | 7 | _error($logger, "::validate_strict: Unknown parameter '$key'"); | |||
268 | } elsif($unknown_parameter_handler eq 'warn') { | |||||
269 | 2 | 5 | _warn($logger, "::validate_strict: Unknown parameter '$key'"); | |||
270 | 2 | 178 | next; | |||
271 | } elsif($unknown_parameter_handler eq 'ignore') { | |||||
272 | 3 | 4 | if($logger) { | |||
273 | 2 | 3 | $logger->debug(__PACKAGE__ . "::validate_strict: Unknown parameter '$key'"); | |||
274 | } | |||||
275 | 3 | 6 | next; | |||
276 | } else { | |||||
277 | 1 | 1 | _error($logger, "::validate_strict: '$unknown_parameter_handler' unknown_parameter_handler must be one of die, warn, ignore"); | |||
278 | } | |||||
279 | } | |||||
280 | } | |||||
281 | ||||||
282 | 169 | 147 | my %validated_args; | |||
283 | 169 169 | 99 160 | foreach my $key (keys %{$schema}) { | |||
284 | 280 | 223 | my $rules = $schema->{$key}; | |||
285 | 280 | 195 | my $value = $args->{$key}; | |||
286 | ||||||
287 | 280 | 238 | if(!defined($rules)) { # Allow anything | |||
288 | 2 | 2 | $validated_args{$key} = $value; | |||
289 | 2 | 2 | next; | |||
290 | } | |||||
291 | ||||||
292 | # If rules are a simple type string | |||||
293 | 278 | 237 | if(ref($rules) eq '') { | |||
294 | 23 | 26 | $rules = { type => $rules }; | |||
295 | } | |||||
296 | ||||||
297 | # Handle optional parameters | |||||
298 | 278 | 429 | if((ref($rules) eq 'HASH') && $rules->{optional}) { | |||
299 | 109 | 95 | if(!exists($args->{$key})) { | |||
300 | 59 | 60 | if($rules->{'default'}) { | |||
301 | # Populate missing optional parameters with the specfied output values | |||||
302 | 2 | 2 | $validated_args{$key} = $rules->{'default'}; | |||
303 | } | |||||
304 | 59 | 54 | next; # optional and missing | |||
305 | } | |||||
306 | } elsif(!exists($args->{$key})) { | |||||
307 | # The parameter is required | |||||
308 | 3 | 8 | _error($logger, "validate_strict: Required parameter '$key' is missing"); | |||
309 | } | |||||
310 | ||||||
311 | # Validate based on rules | |||||
312 | 216 | 186 | if(ref($rules) eq 'HASH') { | |||
313 | 214 | 267 | if((my $min = $rules->{'min'}) && (my $max = $rules->{'max'})) { | |||
314 | 20 | 23 | if($min > $max) { | |||
315 | 4 | 8 | _error($logger, "validate_strict($key): min must be <= max ($min > $max)"); | |||
316 | } | |||||
317 | } | |||||
318 | ||||||
319 | 210 | 171 | if($rules->{'memberof'}) { | |||
320 | 13 | 15 | if(my $min = $rules->{'min'}) { | |||
321 | 0 | 0 | _error($logger, "validate_strict($key): min ($min) makes no sense with memberof"); | |||
322 | } | |||||
323 | 13 | 13 | if(my $max = $rules->{'max'}) { | |||
324 | 0 | 0 | _error($logger, "validate_strict($key): max ($max) makes no sense with memberof"); | |||
325 | } | |||||
326 | } | |||||
327 | ||||||
328 | 210 | 189 | foreach my $rule_name (keys %$rules) { | |||
329 | 394 | 283 | my $rule_value = $rules->{$rule_name}; | |||
330 | ||||||
331 | 394 | 524 | if($rule_name eq 'type') { | |||
332 | 176 | 127 | my $type = lc($rule_value); | |||
333 | ||||||
334 | 176 | 219 | if($type eq 'string') { | |||
335 | 75 | 66 | if(ref($value)) { | |||
336 | 4 | 4 | if($rules->{'error_message'}) { | |||
337 | 1 | 2 | _error($logger, $rules->{'error_message'}); | |||
338 | } else { | |||||
339 | 3 | 4 | _error($logger, "validate_strict: Parameter '$key' must be a string"); | |||
340 | } | |||||
341 | } | |||||
342 | 71 | 80 | unless((ref($value) eq '') || (defined($value) && length($value))) { # Allow undef for optional strings | |||
343 | 0 | 0 | if($rules->{'error_message'}) { | |||
344 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
345 | } else { | |||||
346 | 0 | 0 | _error($logger, "validate_strict: Parameter '$key' must be a string"); | |||
347 | } | |||||
348 | } | |||||
349 | } elsif($type eq 'integer') { | |||||
350 | 25 | 26 | if(!defined($value)) { | |||
351 | 1 | 1 | next; # Skip if number is undefined | |||
352 | } | |||||
353 | 24 | 54 | if($value !~ /^\s*[+\-]?\d+\s*$/) { | |||
354 | 1 | 1 | if($rules->{'error_message'}) { | |||
355 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
356 | } else { | |||||
357 | 1 | 3 | _error($logger, "validate_strict: Parameter '$key' ($value) must be an integer"); | |||
358 | } | |||||
359 | } | |||||
360 | 23 | 23 | $value = int($value); # Coerce to integer | |||
361 | } elsif($type eq 'number') { | |||||
362 | 18 | 17 | if(!defined($value)) { | |||
363 | 2 | 2 | next; # Skip if string is undefined | |||
364 | } | |||||
365 | 16 | 22 | if(!Scalar::Util::looks_like_number($value)) { | |||
366 | 2 | 3 | if($rules->{'error_message'}) { | |||
367 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
368 | } else { | |||||
369 | 2 | 2 | _error($logger, "validate_strict: Parameter '$key' must be a number"); | |||
370 | } | |||||
371 | } | |||||
372 | # $value = eval $value; # Coerce to number (be careful with eval) | |||||
373 | 14 | 20 | $value = 0 + $value; # Numeric coercion | |||
374 | } elsif($type eq 'arrayref') { | |||||
375 | 19 | 19 | if(!defined($value)) { | |||
376 | 2 | 2 | next; # Skip if arrayref is undefined | |||
377 | } | |||||
378 | 17 | 23 | if(ref($value) ne 'ARRAY') { | |||
379 | 0 | 0 | if($rules->{'error_message'}) { | |||
380 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
381 | } else { | |||||
382 | 0 | 0 | _error($logger, "validate_strict: Parameter '$key' must be an arrayref, not " . ref($value)); | |||
383 | } | |||||
384 | } | |||||
385 | } elsif($type eq 'hashref') { | |||||
386 | 21 | 13 | if(!defined($value)) { | |||
387 | 2 | 2 | next; # Skip if hashref is undefined | |||
388 | } | |||||
389 | 19 | 20 | if(ref($value) ne 'HASH') { | |||
390 | 0 | 0 | if($rules->{'error_message'}) { | |||
391 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
392 | } else { | |||||
393 | 0 | 0 | _error($logger, "validate_strict: Parameter '$key' must be an hashref"); | |||
394 | } | |||||
395 | } | |||||
396 | } elsif($type eq 'boolean') { | |||||
397 | 2 | 2 | if(!defined($value)) { | |||
398 | 0 | 0 | next; # Skip if bool is undefined | |||
399 | } | |||||
400 | 2 | 11 | if(($value eq 'true') || ($value eq 'on')) { | |||
401 | 0 | 0 | $value = 1; | |||
402 | } elsif(($value eq 'false') || ($value eq 'off')) { | |||||
403 | 0 | 0 | $value = 0; | |||
404 | } | |||||
405 | 2 | 3 | if(($value != 1) && ($value != 0)) { | |||
406 | 1 | 1 | if($rules->{'error_message'}) { | |||
407 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
408 | } else { | |||||
409 | 1 | 1 | _error($logger, "validate_strict: Parameter '$key' ($value) must be a boolean"); | |||
410 | } | |||||
411 | } | |||||
412 | 1 | 1 | $value = int($value); # Coerce to integer | |||
413 | } elsif($type eq 'coderef') { | |||||
414 | 4 | 6 | if(ref($value) ne 'CODE') { | |||
415 | 1 | 1 | if($rules->{'error_message'}) { | |||
416 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
417 | } else { | |||||
418 | 1 | 2 | _error($logger, "validate_strict: Parameter '$key' must be a coderef"); | |||
419 | } | |||||
420 | } | |||||
421 | } elsif($type eq 'object') { | |||||
422 | 10 | 21 | if(!Scalar::Util::blessed($value)) { | |||
423 | 1 | 1 | if($rules->{'error_message'}) { | |||
424 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
425 | } else { | |||||
426 | 1 | 2 | _error($logger, "validate_strict: Parameter '$key' must be an object"); | |||
427 | } | |||||
428 | } | |||||
429 | } else { | |||||
430 | 2 | 3 | _error($logger, "validate_strict: Unknown type '$type'"); | |||
431 | } | |||||
432 | } elsif($rule_name eq 'min') { | |||||
433 | 47 | 42 | if(!defined($rules->{'type'})) { | |||
434 | 0 | 0 | _error($logger, "validate_strict: Don't know type of '$key' to determine its minimum value $rule_value"); | |||
435 | } | |||||
436 | 47 | 89 | if($rules->{'type'} eq 'string') { | |||
437 | 15 | 15 | if(!defined($value)) { | |||
438 | 0 | 0 | next; # Skip if string is undefined | |||
439 | } | |||||
440 | 15 | 16 | if(length($value) < $rule_value) { | |||
441 | 2 | 3 | if($rules->{'error_message'}) { | |||
442 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
443 | } else { | |||||
444 | 2 | 5 | _error($logger, "validate_strict: String parameter '$key' too short, must be at least length $rule_value"); | |||
445 | } | |||||
446 | } | |||||
447 | } elsif($rules->{'type'} eq 'arrayref') { | |||||
448 | 6 | 9 | if(!defined($value)) { | |||
449 | 1 | 1 | next; # Skip if array is undefined | |||
450 | } | |||||
451 | 5 | 6 | if(ref($value) ne 'ARRAY') { | |||
452 | 0 | 0 | if($rules->{'error_message'}) { | |||
453 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
454 | } else { | |||||
455 | 0 | 0 | _error($logger, "validate_strict: Parameter '$key' must be an arrayref, not " . ref($value)); | |||
456 | } | |||||
457 | } | |||||
458 | 5 5 | 4 9 | if(scalar(@{$value}) < $rule_value) { | |||
459 | 1 | 2 | if($rules->{'error_message'}) { | |||
460 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
461 | } else { | |||||
462 | 1 | 2 | _error($logger, "validate_strict: Parameter '$key' must be at least length $rule_value"); | |||
463 | } | |||||
464 | } | |||||
465 | } elsif($rules->{'type'} eq 'hashref') { | |||||
466 | 4 | 4 | if(!defined($value)) { | |||
467 | 0 | 0 | next; # Skip if hash is undefined | |||
468 | } | |||||
469 | 4 4 | 3 6 | if(scalar(keys(%{$value})) < $rule_value) { | |||
470 | 1 | 2 | if($rules->{'error_message'}) { | |||
471 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
472 | } else { | |||||
473 | 1 | 3 | _error($logger, "validate_strict: Parameter '$key' must contain at least $rule_value keys"); | |||
474 | } | |||||
475 | } | |||||
476 | } elsif(($rules->{'type'} eq 'integer') || ($rules->{'type'} eq 'number')) { | |||||
477 | 21 | 19 | if(!defined($value)) { | |||
478 | 1 | 1 | next; # Skip if hash is undefined | |||
479 | } | |||||
480 | 20 | 24 | if($value < $rule_value) { | |||
481 | 5 | 8 | if($rules->{'error_message'}) { | |||
482 | 1 | 1 | _error($logger, $rules->{'error_message'}); | |||
483 | } else { | |||||
484 | 4 | 9 | _error($logger, "validate_strict: Parameter '$key' must be at least $rule_value"); | |||
485 | } | |||||
486 | } | |||||
487 | } else { | |||||
488 | 1 | 2 | _error($logger, "validate_strict: Parameter '$key' has meaningless min value $rule_value"); | |||
489 | } | |||||
490 | } elsif($rule_name eq 'max') { | |||||
491 | 32 | 31 | if(!defined($rules->{'type'})) { | |||
492 | 0 | 0 | _error($logger, "validate_strict: Don't know type of '$key' to determine its maximum value $rule_value"); | |||
493 | } | |||||
494 | 32 | 55 | if($rules->{'type'} eq 'string') { | |||
495 | 10 | 13 | if(!defined($value)) { | |||
496 | 0 | 0 | next; # Skip if string is undefined | |||
497 | } | |||||
498 | 10 | 7 | if(length($value) > $rule_value) { | |||
499 | 4 | 5 | if($rules->{'error_message'}) { | |||
500 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
501 | } else { | |||||
502 | 4 | 9 | _error($logger, "validate_strict: String parameter '$key' too long, (" . length($value) . " characters), must be no longer than $rule_value"); | |||
503 | } | |||||
504 | } | |||||
505 | } elsif($rules->{'type'} eq 'arrayref') { | |||||
506 | 6 | 5 | if(!defined($value)) { | |||
507 | 0 | 0 | next; # Skip if string is undefined | |||
508 | } | |||||
509 | 6 | 6 | if(ref($value) ne 'ARRAY') { | |||
510 | 0 | 0 | if($rules->{'error_message'}) { | |||
511 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
512 | } else { | |||||
513 | 0 | 0 | _error($logger, "validate_strict: Parameter '$key' must be an arrayref, not " . ref($value)); | |||
514 | } | |||||
515 | } | |||||
516 | 6 6 | 6 7 | if(scalar(@{$value}) > $rule_value) { | |||
517 | 3 | 3 | if($rules->{'error_message'}) { | |||
518 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
519 | } else { | |||||
520 | 3 | 4 | _error($logger, "validate_strict: Parameter '$key' must contain no more than $rule_value items"); | |||
521 | } | |||||
522 | } | |||||
523 | } elsif($rules->{'type'} eq 'hashref') { | |||||
524 | 4 | 7 | if(!defined($value)) { | |||
525 | 1 | 1 | next; # Skip if hash is undefined | |||
526 | } | |||||
527 | 3 3 | 1 3 | if(scalar(keys(%{$value})) > $rule_value) { | |||
528 | 2 | 2 | if($rules->{'error_message'}) { | |||
529 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
530 | } else { | |||||
531 | 2 | 10 | _error($logger, "validate_strict: Parameter '$key' must contain no more than $rule_value keys"); | |||
532 | } | |||||
533 | } | |||||
534 | } elsif(($rules->{'type'} eq 'integer') || ($rules->{'type'} eq 'number')) { | |||||
535 | 11 | 8 | if(!defined($value)) { | |||
536 | 0 | 0 | next; # Skip if hash is undefined | |||
537 | } | |||||
538 | 11 | 10 | if($value > $rule_value) { | |||
539 | 1 | 2 | if($rules->{'error_message'}) { | |||
540 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
541 | } else { | |||||
542 | 1 | 4 | _error($logger, "validate_strict: Parameter '$key' ($value) must be no more than $rule_value"); | |||
543 | } | |||||
544 | } | |||||
545 | } else { | |||||
546 | 1 | 2 | _error($logger, "validate_strict: Parameter '$key' has meaningless max value $rule_value"); | |||
547 | } | |||||
548 | } elsif($rule_name eq 'matches') { | |||||
549 | 20 | 27 | if(!defined($value)) { | |||
550 | 1 | 1 | next; # Skip if string is undefined | |||
551 | } | |||||
552 | 19 | 13 | eval { | |||
553 | 19 | 92 | if($rules->{'type'} eq 'arrayref') { | |||
554 | 3 6 3 | 2 14 3 | my @matches = grep { /$rule_value/ } @{$value}; | |||
555 | 3 3 | 3 6 | if(scalar(@matches) != scalar(@{$value})) { | |||
556 | 1 | 1 | if($rules->{'error_message'}) { | |||
557 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
558 | } else { | |||||
559 | 1 1 | 1 2 | _error($logger, "validate_strict: All members of parameter '$key' [", join(', ', @{$value}), "] must match pattern '$rule_value'"); | |||
560 | } | |||||
561 | } | |||||
562 | } elsif($value !~ $rule_value) { | |||||
563 | 5 | 5 | if($rules->{'error_message'}) { | |||
564 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
565 | } else { | |||||
566 | 5 | 12 | _error($logger, "validate_strict: Parameter '$key' ($value) must match pattern '$rule_value'"); | |||
567 | } | |||||
568 | } | |||||
569 | }; | |||||
570 | 19 | 8703 | if($@) { | |||
571 | 9 | 24 | _error($logger, "validate_strict: Parameter '$key' invalid regex '$rule_value': $@"); | |||
572 | } | |||||
573 | } elsif($rule_name eq 'nomatch') { | |||||
574 | 8 | 22 | if($rules->{'type'} eq 'arrayref') { | |||
575 | 4 11 4 | 2 19 4 | my @matches = grep { /$rule_value/ } @{$value}; | |||
576 | 4 | 5 | if(scalar(@matches)) { | |||
577 | 2 | 3 | if($rules->{'error_message'}) { | |||
578 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
579 | } else { | |||||
580 | 2 2 | 2 6 | _error($logger, "validate_strict: No member of parameter '$key' [", join(', ', @{$value}), "] must match pattern '$rule_value'"); | |||
581 | } | |||||
582 | } | |||||
583 | } elsif($value =~ $rule_value) { | |||||
584 | 1 | 2 | if($rules->{'error_message'}) { | |||
585 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
586 | } else { | |||||
587 | 1 | 2 | _error($logger, "validate_strict: Parameter '$key' ($value) must not match pattern '$rule_value'"); | |||
588 | } | |||||
589 | } | |||||
590 | } elsif($rule_name eq 'memberof') { | |||||
591 | 13 | 14 | if(!defined($value)) { | |||
592 | 0 | 0 | next; # Skip if string is undefined | |||
593 | } | |||||
594 | 13 | 17 | if(ref($rule_value) eq 'ARRAY') { | |||
595 | 12 | 23 | if(($rules->{'type'} eq 'integer') || ($rules->{'type'} eq 'number')) { | |||
596 | 7 20 7 | 11 20 10 | unless(List::Util::any { $_ == $value } @{$rule_value}) { | |||
597 | 3 | 3 | if($rules->{'error_message'}) { | |||
598 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
599 | } else { | |||||
600 | 3 3 | 4 7 | _error($logger, "validate_strict: Parameter '$key' ($value) must be one of ", join(', ', @{$rule_value})); | |||
601 | } | |||||
602 | } | |||||
603 | } else { | |||||
604 | 5 8 5 | 9 12 9 | unless(List::Util::any { $_ eq $value } @{$rule_value}) { | |||
605 | 2 | 3 | if($rules->{'error_message'}) { | |||
606 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
607 | } else { | |||||
608 | 2 2 | 2 4 | _error($logger, "validate_strict: Parameter '$key' ($value) must be one of ", join(', ', @{$rule_value})); | |||
609 | } | |||||
610 | } | |||||
611 | } | |||||
612 | } else { | |||||
613 | 1 | 1 | if($rules->{'error_message'}) { | |||
614 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
615 | } else { | |||||
616 | 1 | 2 | _error($logger, "validate_strict: Parameter '$key' rule ($rule_value) must be an array reference"); | |||
617 | } | |||||
618 | } | |||||
619 | } elsif ($rule_name eq 'callback') { | |||||
620 | 14 | 16 | unless (defined &$rule_value) { | |||
621 | 1 | 1 | _error($logger, "validate_strict: callback for '$key' must be a code reference"); | |||
622 | } | |||||
623 | 13 | 13 | my $res = $rule_value->($value); | |||
624 | 12 | 2210 | unless ($res) { | |||
625 | 4 | 6 | if($rules->{'error_message'}) { | |||
626 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
627 | } else { | |||||
628 | 4 | 4 | _error($logger, "validate_strict: Parameter '$key' failed custom validation"); | |||
629 | } | |||||
630 | } | |||||
631 | } elsif($rule_name eq 'isa') { | |||||
632 | 4 | 3 | if($rules->{'type'} eq 'object') { | |||
633 | 3 | 8 | if(!$value->isa($rule_value)) { | |||
634 | 1 | 1 | _error($logger, "validate_strict: Parameter '$key' must be a '$rule_value' object"); | |||
635 | } | |||||
636 | } else { | |||||
637 | 1 | 1 | _error($logger, "validate_strict: Parameter '$key' has meaningless isa value $rule_value"); | |||
638 | } | |||||
639 | } elsif($rule_name eq 'can') { | |||||
640 | 10 | 12 | if($rules->{'type'} eq 'object') { | |||
641 | 9 | 13 | if(ref($rule_value) eq 'ARRAY') { | |||
642 | # List of methods | |||||
643 | 4 4 | 3 11 | foreach my $method(@{$rule_value}) { | |||
644 | 6 | 16 | if(!$value->can($method)) { | |||
645 | 2 | 3 | _error($logger, "validate_strict: Parameter '$key' must be an object that understands the $method method"); | |||
646 | } | |||||
647 | } | |||||
648 | } elsif(!ref($rule_value)) { | |||||
649 | 4 | 18 | if(!$value->can($rule_value)) { | |||
650 | 2 | 4 | _error($logger, "validate_strict: Parameter '$key' must be an object that understands the $rule_value method"); | |||
651 | } | |||||
652 | } else { | |||||
653 | 1 | 3 | _error($logger, "validate_strict: 'can' rule for Parameter '$key must be either a scalar or an arrayref"); | |||
654 | } | |||||
655 | } else { | |||||
656 | 1 | 2 | _error($logger, "validate_strict: Parameter '$key' has meaningless can value $rule_value"); | |||
657 | } | |||||
658 | } elsif($rule_name eq 'element_type') { | |||||
659 | 5 | 5 | if($rules->{'type'} eq 'arrayref') { | |||
660 | 5 5 | 3 4 | foreach my $member(@{$value}) { | |||
661 | 12 | 34 | if($rule_value eq 'string') { | |||
662 | 5 | 4 | if(ref($member)) { | |||
663 | 0 | 0 | if($rules->{'error_message'}) { | |||
664 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
665 | } else { | |||||
666 | 0 | 0 | _error($logger, "$key can only contain strings"); | |||
667 | } | |||||
668 | } | |||||
669 | } elsif($rule_value eq 'integer') { | |||||
670 | 3 | 5 | if(ref($member) || ($member =~ /\D/)) { | |||
671 | 0 | 0 | if($rules->{'error_message'}) { | |||
672 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
673 | } else { | |||||
674 | 0 | 0 | _error($logger, "$key can only contain numbers (found $member)"); | |||
675 | } | |||||
676 | } | |||||
677 | } elsif($rule_value eq 'number') { | |||||
678 | 4 | 12 | if(ref($member) || ($member !~ /^[-+]?(\d*\.\d+|\d+\.?\d*)$/)) { | |||
679 | 1 | 1 | if($rules->{'error_message'}) { | |||
680 | 0 | 0 | _error($logger, $rules->{'error_message'}); | |||
681 | } else { | |||||
682 | 1 | 1 | _error($logger, "$key can only contain numbers (found $member)"); | |||
683 | } | |||||
684 | } | |||||
685 | } | |||||
686 | } | |||||
687 | } else { | |||||
688 | 0 | 0 | _error($logger, "validate_strict: Parameter '$key' has meaningless element_type value $rule_value"); | |||
689 | } | |||||
690 | } elsif($rule_name eq 'optional') { | |||||
691 | # Already handled at the beginning of the loop | |||||
692 | } elsif($rule_name eq 'default') { | |||||
693 | # Handled earlier | |||||
694 | } elsif($rule_name eq 'error_message') { | |||||
695 | # Handled in line | |||||
696 | } elsif($rule_name eq 'schema') { | |||||
697 | # Nested schema Run the given schema against each element of the array | |||||
698 | 19 | 16 | if($rules->{'type'} eq 'arrayref') { | |||
699 | 5 | 6 | if(ref($value) eq 'ARRAY') { | |||
700 | 4 4 | 2 3 | foreach my $member(@{$value}) { | |||
701 | 6 | 21 | validate_strict({ input => { $key => $member }, schema => { $key => $rule_value } }); | |||
702 | } | |||||
703 | } elsif(defined($value)) { # Allow undef for optional values | |||||
704 | 1 | 1 | _error($logger, "validate_strict: nested schema: Parameter '$value' must be an arrayref"); | |||
705 | } | |||||
706 | } elsif($rules->{'type'} eq 'hashref') { | |||||
707 | 14 | 12 | if(ref($value) eq 'HASH') { | |||
708 | 14 14 | 8 12 | if(scalar keys(%{$value})) { | |||
709 | 13 | 41 | validate_strict({ input => $value, schema => $rule_value }); | |||
710 | } | |||||
711 | } else { | |||||
712 | 0 | 0 | _error($logger, "validate_strict: nested schema: Parameter '$value' must be an hashref"); | |||
713 | } | |||||
714 | } else { | |||||
715 | 0 | 0 | _error($logger, "validate_strict: Parameter '$key': 'schema' only supports arrayref and hashref, not $rules->{type}"); | |||
716 | } | |||||
717 | } else { | |||||
718 | 1 | 12 | _error($logger, "validate_strict: Unknown rule '$rule_name'"); | |||
719 | } | |||||
720 | } | |||||
721 | } elsif(ref($rules)) { | |||||
722 | 2 | 6 | _error($logger, 'rules must be a hash reference or string'); | |||
723 | } | |||||
724 | ||||||
725 | 136 | 157 | $validated_args{$key} = $value; | |||
726 | } | |||||
727 | ||||||
728 | 86 | 211 | return \%validated_args; | |||
729 | } | |||||
730 | ||||||
731 | # Helper to log error or croak | |||||
732 | sub _error | |||||
733 | { | |||||
734 | 91 | 73 | my $logger = shift; | |||
735 | 91 | 97 | my $message = join('', @_); | |||
736 | ||||||
737 | 91 | 100 | my @call_details = caller(0); | |||
738 | 91 | 1239 | if($logger) { | |||
739 | 3 | 5 | $logger->error(__PACKAGE__, ' line ', $call_details[2], ": $message"); | |||
740 | } else { | |||||
741 | 88 | 390 | croak(__PACKAGE__, ' line ', $call_details[2], ": $message"); | |||
742 | } | |||||
743 | } | |||||
744 | ||||||
745 | # Helper to log warning or carp | |||||
746 | sub _warn | |||||
747 | { | |||||
748 | 2 | 2 | my $logger = shift; | |||
749 | 2 | 4 | my $message = join('', @_); | |||
750 | ||||||
751 | 2 | 2 | if($logger) { | |||
752 | 1 | 2 | $logger->warn(__PACKAGE__, ": $message"); | |||
753 | } else { | |||||
754 | 1 | 4 | carp(__PACKAGE__ . ": $message"); | |||
755 | } | |||||
756 | } | |||||
757 | ||||||
758 - 865 | =head1 AUTHOR Nigel Horne, C<< <njh at nigelhorne.com> >> =encoding utf-8 =head1 FORMAL SPECIFICATION [PARAM_NAME, VALUE, TYPE_NAME, CONSTRAINT_VALUE] ValidationRule ::= SimpleType | ComplexRule SimpleType ::= string | integer | number | arrayref | hashref | coderef | object ComplexRule == [ type: TYPE_NAME; min: ââ; max: ââ; optional: ð¹; matches: REGEX; nomatch: REGEX; memberof: seq VALUE; callback: FUNCTION; isa: TYPE_NAME; can: METHOD_NAME ] Schema == PARAM_NAME ⸠ValidationRule Arguments == PARAM_NAME ⸠VALUE ValidatedResult == PARAM_NAME ⸠VALUE â â rule: ComplexRule ⢠rule.min ⤠rule.max â â schema: Schema; args: Arguments ⢠â dom(validate_strict(schema, args)) â dom(schema) ⪠dom(args) validate_strict: Schema à Arguments â ValidatedResult â schema: Schema; args: Arguments ⢠let result == validate_strict(schema, args) ⢠(â name: dom(schema) â© dom(args) ⢠name â dom(result) â type_matches(result(name), schema(name))) â§ (â name: dom(schema) ⢠¬optional(schema(name)) â name â dom(args)) type_matches: VALUE à ValidationRule â ð¹ =head1 BUGS =head1 SEE ALSO =over 4 =item * Test coverage report: L<https://nigelhorne.github.io/Params-Validate-Strict/coverage/> =item * L<Params::Get> =item * L<Params::Validate> =item * L<Return::Set> =back =head1 SUPPORT This module is provided as-is without any warranty. Please report any bugs or feature requests to C<bug-params-validate-strict at rt.cpan.org>, or through the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Params-Validate-Strict>. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes. You can find documentation for this module with the perldoc command. perldoc Params::Validate::Strict You can also look for information at: =over 4 =item * MetaCPAN L<https://metacpan.org/dist/Params-Validate-Strict> =item * RT: CPAN's request tracker L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Params-Validate-Strict> =item * CPAN Testers' Matrix L<http://matrix.cpantesters.org/?dist=Params-Validate-Strict> =item * CPAN Testers Dependencies L<http://deps.cpantesters.org/?module=Params::Validate::Strict> =back =head1 LICENSE AND COPYRIGHT Copyright 2025 Nigel Horne. This program is released under the following licence: GPL2 =cut | |||||
866 | ||||||
867 | 1; | |||||
868 |