TER1 (Statement): 86.55%
TER2 (Branch): 86.67%
TER3 (LCSAJ): 100.0% (24/24)
Approximate LCSAJ segments: 631
● Covered — this LCSAJ path was executed during testing.
● Not covered — this LCSAJ path was never executed. These are the paths to focus on.
Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:
start → end → jump
Uncovered paths show [NOT COVERED] in the tooltip.
1: package Params::Validate::Strict; 2: 3: # FIXME: {max} doesn't play ball with non-ascii strings 4: # TODO: better use of the description parameter in error messages 5: # FIXME: ensure paramaters such as min => 1 length constraint applies to all values. In this case, undef should not pass through without a croak. 6: 7: use strict; 8: use warnings; 9: 10: use Carp; 11: use Exporter qw(import); # Required for @EXPORT_OK 12: use Encode qw(decode_utf8); 13: use List::Util 1.33 qw(any); # Required for memberof validation 14: use Params::Get 0.13; 15: use Readonly::Values::Boolean; 16: use Scalar::Util; 17: use Unicode::GCString; 18: 19: our @ISA = qw(Exporter); 20: our @EXPORT_OK = qw(validate_strict); 21: 22: =head1 NAME 23: 24: Params::Validate::Strict - Validates a set of parameters against a schema 25: 26: =head1 VERSION 27: 28: Version 0.34 29: 30: =cut 31: 32: our $VERSION = '0.34'; 33: 34: =head1 SYNOPSIS 35: 36: my $schema = { 37: username => { type => 'string', min => 3, max => 50 }, 38: age => { type => 'integer', min => 0, max => 150 }, 39: }; 40: 41: my $input = { 42: username => 'john_doe', 43: age => '30', # Will be coerced to integer 44: }; 45: 46: my $validated_input = validate_strict(schema => $schema, input => $input); 47: 48: if(defined($validated_input)) { 49: print "Example 1: Validation successful!\n"; 50: print 'Username: ', $validated_input->{username}, "\n"; 51: print 'Age: ', $validated_input->{age}, "\n"; # It's an integer now 52: } else { 53: print "Example 1: Validation failed: $@\n"; 54: } 55: 56: Upon first reading this may seem overly complex and full of scope creep in a sledgehammer to crack a nut sort of way, 57: however two use cases make use of the extensive logic that comes with this code 58: and I have a couple of other reasons for writing it. 59: 60: =over 4 61: 62: =item * Black Box Testing 63: 64: The schema can be plumbed into L<App::Test::Generator> to automatically create a set of black-box test cases. 65: 66: =item * WAF 67: 68: The schema can be plumbed into a WAF to protect from random user input. 69: 70: =item * Improved API Documentation 71: 72: Even if you don't use this module, 73: the specification syntax can help with documentation. 74: 75: =item * I like it 76: 77: I find it fun to write this, 78: even if nobody else finds it useful, 79: though I hope you will. 80: 81: =back 82: 83: =head1 METHODS 84: 85: =head2 validate_strict 86: 87: Validates a set of parameters against a schema. 88: 89: This function takes two mandatory arguments: 90: 91: =over 4 92: 93: =item * C<schema> || C<members> 94: 95: A reference to a hash that defines the validation rules for each parameter. 96: 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. 97: 98: As an alternative the schema may be supplied as an B<arrayref of parameter 99: hashrefs>, where every element describes one parameter and carries a mandatory 100: C<name> key: 101: 102: $schema = [ 103: { name => 'username', type => 'string', min => 3, max => 50 }, 104: { name => 'age', type => 'integer', min => 0, max => 150 }, 105: { name => 'role', type => 'string', optional => 1, default => 'user' }, 106: ]; 107: 108: The arrayref form is normalised to the standard hashref form before any further 109: processing. It is particularly useful when declaration order matters (e.g. 110: for positional or mixed calling conventions used by some CPAN modules). The 111: C<name> key is consumed during normalisation and does not appear as a 112: validation rule. 113: 114: For some sort of compatibility with L<Data::Processor>, 115: it is possible to wrap the schema within a hash like this: 116: 117: $schema = { 118: description => 'Describe what this schema does', 119: error_msg => 'An error message', 120: schema => { 121: # ... schema goes here 122: } 123: } 124: 125: =item * C<args> || C<input> 126: 127: A reference to a hash containing the parameters to be validated. 128: The keys of the hash are the parameter names, and the values are the parameter values. 129: 130: =back 131: 132: It takes optional arguments: 133: 134: =over 4 135: 136: =item * C<description> 137: 138: What the schema does, 139: used in error messages. 140: 141: =item * C<error_msg> 142: 143: Overrides the default message when something doesn't validate. 144: 145: =item * C<unknown_parameter_handler> 146: 147: This parameter describes what to do when a parameter is given that is not in the schema of valid parameters. 148: It must be one of C<die>, C<warn>, or C<ignore>. 149: 150: It defaults to C<die> unless C<carp_on_warn> is given, in which case it defaults to C<warn>. 151: 152: =item * C<logger> 153: 154: A logging object that understands messages such as C<error> and C<warn>. 155: 156: =item * C<custom_types> 157: 158: A reference to a hash that defines reusable custom types. 159: Custom types allow you to define validation rules once and reuse them throughout your schema, 160: making your validation logic more maintainable and readable. 161: 162: Each custom type is defined as a hash reference containing the same validation rules available for regular parameters 163: (C<type>, C<min>, C<max>, C<matches>, C<memberof>, C<values>, C<enum>, C<notmemberof>, C<callback>, etc.). 164: 165: my $custom_types = { 166: email => { 167: type => 'string', 168: matches => qr/^[\w\.\-]+@[\w\.\-]+\.\w+$/, 169: error_msg => 'Invalid email address format' 170: }, phone => { 171: type => 'string', 172: matches => qr/^\+?[1-9]\d{1,14}$/, 173: min => 10, 174: max => 15 175: }, percentage => { 176: type => 'number', 177: min => 0, 178: max => 100 179: }, status => { 180: type => 'string', 181: memberof => ['draft', 'published', 'archived'] 182: } 183: }; 184: 185: my $schema = { 186: user_email => { type => 'email' }, 187: contact_number => { type => 'phone', optional => 1 }, 188: completion => { type => 'percentage' }, 189: post_status => { type => 'status' } 190: }; 191: 192: my $validated = validate_strict( 193: schema => $schema, 194: input => $input, 195: custom_types => $custom_types 196: ); 197: 198: Custom types can be extended or overridden in the schema by specifying additional constraints: 199: 200: my $schema = { 201: admin_username => { 202: type => 'username', # Uses custom type definition 203: min => 5, # Overrides custom type's min value 204: max => 15 # Overrides custom type's max value 205: } 206: }; 207: 208: Custom types work seamlessly with nested schema, optional parameters, and all other validation features. 209: 210: =back 211: 212: The schema can define the following rules for each parameter: 213: 214: =over 4 215: 216: =item * C<type> 217: 218: The data type of the parameter. 219: Valid types are C<string>, C<integer>, C<number>, C<float> C<boolean>, C<scalar>, C<scalarref>, C<stringref>, C<hashref>, C<arrayref>, C<object> and C<coderef>. 220: C<scalar> accepts any plain scalar value (string, number, boolean, etc.) but rejects references (arrayrefs, hashrefs, coderefs, objects). 221: C<scalarref> accepts a reference to a scalar value (e.g. C<\$var>) but rejects plain scalars, arrayrefs, hashrefs, coderefs, and objects. 222: C<stringref> accepts a reference to a scalar that contains a plain string (e.g. C<\$str>) and rejects plain scalars, references-to-references, arrayrefs, hashrefs, coderefs, and objects. 223: The C<min>/C<max> constraints apply to the B<length> (in characters) of the referenced string. 224: All other string rules (C<matches>, C<nomatch>, C<memberof>, etc.) operate on the dereferenced string value. 225: The validated return value is the dereferenced plain string. 226: 227: A type can be an arrayref when a parameter could have different types (e.g. a string or an object). 228: 229: $schema = { 230: username => [ 231: { type => 'string', min => 3, max => 50 }, # Name 232: { type => 'integer', 'min' => 1 }, # UID that isn't root 233: ] 234: }; 235: 236: As a shorthand, C<type> itself may be an arrayref of type name strings (a I<union type>) 237: when all other constraints are shared between the alternatives: 238: 239: $schema = { 240: data => { type => ['string', 'arrayref'] }, 241: id => { type => ['string', 'integer'], optional => 1 }, 242: }; 243: 244: This is equivalent to the full array-of-rules form but more concise. 245: Every other key in the rule hash (C<optional>, C<min>, C<max>, C<matches>, etc.) 246: is inherited by each candidate type and validated independently against it. 247: Type names are tried left-to-right; the first match wins and its coercion 248: (e.g. numeric types) is propagated back to the caller. 249: If the value fails all candidate types, validation croaks with a message 250: listing the union members. 251: 252: =item * C<can> 253: 254: The parameter must be an object that understands the method C<can>. 255: C<can> can be a simple scalar string of a method name, 256: or an arrayref of a list of method names, all of which must be supported by the object. 257: 258: $schema = { 259: gedcom => { type => object, can => 'get_individual' } 260: } 261: 262: =item * C<isa> 263: 264: The parameter must be an object of type C<isa>. 265: 266: =item * C<memberof> 267: 268: The parameter must be a member of the given arrayref. 269: 270: status => { 271: type => 'string', 272: memberof => ['draft', 'published', 'archived'] 273: } 274: 275: priority => { 276: type => 'integer', 277: memberof => [1, 2, 3, 4, 5] 278: } 279: 280: For string types, the comparison is case-sensitive by default. Use the C<case_sensitive> 281: flag to control this behavior: 282: 283: # Case-sensitive (default) - must be exact match 284: code => { 285: type => 'string', 286: memberof => ['ABC', 'DEF', 'GHI'] 287: # 'abc' will fail 288: } 289: 290: # Case-insensitive - any case accepted 291: code => { 292: type => 'string', 293: memberof => ['ABC', 'DEF', 'GHI'], 294: case_sensitive => 0 295: # 'abc', 'Abc', 'ABC' all pass, original case preserved 296: } 297: 298: For numeric types (C<integer>, C<number>, C<float>), the comparison uses numeric 299: equality (C<==> operator): 300: 301: rating => { 302: type => 'number', 303: memberof => [0.5, 1.0, 1.5, 2.0] 304: } 305: 306: Note that C<memberof> cannot be combined with C<min> or C<max> constraints as they 307: serve conflicting purposes - C<memberof> defines an explicit whitelist while C<min>/C<max> 308: define ranges. 309: 310: =item * C<enum> 311: 312: Same as C<memberof>. 313: 314: =item * C<values> 315: 316: Same as C<memberof>. 317: 318: =item * C<notmemberof> 319: 320: The parameter must not be a member of the given arrayref (blacklist). 321: This is the inverse of C<memberof>. 322: 323: username => { 324: type => 'string', 325: notmemberof => ['admin', 'root', 'system', 'administrator'] 326: } 327: 328: port => { 329: type => 'integer', 330: notmemberof => [22, 23, 25, 80, 443] # Reserved ports 331: } 332: 333: Like C<memberof>, string comparisons are case-sensitive by default but can be controlled 334: with the C<case_sensitive> flag: 335: 336: # Case-sensitive (default) 337: username => { 338: type => 'string', 339: notmemberof => ['Admin', 'Root'] 340: # 'admin' would pass, 'Admin' would fail 341: } 342: 343: # Case-insensitive 344: username => { 345: type => 'string', 346: notmemberof => ['Admin', 'Root'], 347: case_sensitive => 0 348: # 'admin', 'ADMIN', 'Admin' all fail 349: } 350: 351: The blacklist is checked after any C<transform> rules are applied, allowing you to 352: normalize input before checking: 353: 354: username => { 355: type => 'string', 356: transform => sub { lc($_[0]) }, # Normalize to lowercase 357: notmemberof => ['admin', 'root', 'system'] 358: } 359: 360: C<notmemberof> can be combined with other validation rules: 361: 362: username => { 363: type => 'string', 364: notmemberof => ['admin', 'root', 'system'], 365: min => 3, 366: max => 20, 367: matches => qr/^[a-z0-9_]+$/ 368: } 369: 370: =item * C<case_sensitive> 371: 372: A boolean value indicating whether string comparisons should be case-sensitive. 373: This flag affects the C<memberof> and C<notmemberof> validation rules. 374: The default value is C<1> (case-sensitive). 375: 376: When set to C<0>, string comparisons are performed case-insensitively, allowing values 377: with different casing to match. The original case of the input value is preserved in 378: the validated output. 379: 380: # Case-sensitive (default) 381: status => { 382: type => 'string', 383: memberof => ['Draft', 'Published', 'Archived'] # Input 'draft' will fail - must match exact case 384: } 385: 386: # Case-insensitive 387: status => { 388: type => 'string', 389: memberof => ['Draft', 'Published', 'Archived'], 390: case_sensitive => 0 # Input 'draft', 'DRAFT', or 'DrAfT' will all pass 391: } 392: 393: country_code => { 394: type => 'string', 395: memberof => ['US', 'UK', 'CA', 'FR'], 396: case_sensitive => 0 # Accept 'us', 'US', 'Us', etc. 397: } 398: 399: This flag has no effect on numeric types (C<integer>, C<number>, C<float>) as numbers 400: do not have case. 401: 402: =item * C<min>/C<minimum> 403: 404: The minimum length (for strings in characters not bytes), value (for numbers) or number of keys (for hashrefs). 405: 406: =item * C<max> 407: 408: The maximum length (for strings in characters not bytes), value (for numbers) or number of keys (for hashrefs). 409: 410: =item * C<matches> 411: 412: A regular expression that the parameter value must match. 413: Checks all members of arrayrefs. 414: 415: =item * C<nomatch> 416: 417: A regular expression that the parameter value must not match. 418: Checks all members of arrayrefs. 419: 420: =item * C<position> 421: 422: For routines and methods that take positional args, 423: this integer value defines which position the argument will be in. 424: If this is set for all arguments, 425: C<validate_strict> will return a reference to an array, rather than a reference to a hash. 426: 427: =item * C<regex> 428: 429: Synonym of matches 430: 431: =item * C<description> 432: 433: The description of the rule 434: 435: =item * C<callback> 436: 437: A code reference to a subroutine that performs custom validation logic. 438: The subroutine should accept the parameter value, the argument list and the schema as arguments and return true if the value is valid, false otherwise. 439: 440: Use this to test more complex examples: 441: 442: my $schema = { 443: even_number => { 444: type => 'integer', 445: callback => sub { $_[0] % 2 == 0 } 446: }; 447: 448: # Specify the arguments for a routine which has a second, optional argument, which, if given, must be less than or equal to the first 449: my $schema = { 450: first => { 451: type => 'integer' 452: }, second => { 453: type => 'integer', 454: optional => 1, 455: callback => sub { 456: my($value, $args) = @_; 457: # The 'defined' is needed in case 'second' is evaluated before 'first' 458: return (defined($args->{first}) && $value <= $args->{first}) ? 1 : 0 459: } 460: } 461: }; 462: 463: =item * C<optional> 464: 465: A boolean value indicating whether the parameter is optional. 466: If true, the parameter is not required. 467: If false or omitted, the parameter is required. 468: 469: It can be a reference to a code snippet that will return true or false, 470: to determine if the parameter is optional or not. 471: The code will be called with two arguments: the value of the parameter and hash ref of all parameters: 472: 473: my $schema = { 474: optional_field => { 475: type => 'string', 476: optional => sub { 477: my ($value, $all_params) = @_; 478: return $all_params->{make_optional} ? 1 : 0; 479: } 480: }, 481: make_optional => { type => 'boolean' } 482: }; 483: 484: my $result = validate_strict(schema => $schema, input => { make_optional => 1 }); 485: 486: If the parameter is not optional, it can be passed an undef value, which will not flag an error. 487: This is by design. 488: So this will not say that the required parameter 's' is missing: 489: 490: validate_strict( 491: schema => { s => { type => 'string' } }, 492: input => { s => undef }, 493: ); 494: 495: =item * C<default> 496: 497: Populate missing optional parameters with the specified value. 498: Note that this value is not validated. 499: 500: username => { 501: type => 'string', 502: optional => 1, 503: default => 'guest' 504: } 505: 506: =item * C<element_type> 507: 508: Extends the validation to individual elements of arrays. 509: 510: tags => { 511: type => 'arrayref', 512: element_type => 'number', # Float means the same 513: 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 514: max => 5 515: } 516: 517: =item * C<error_msg> 518: 519: The custom error message to be used in the event of a validation failure. 520: 521: age => { 522: type => 'integer', 523: min => 18, 524: error_msg => 'You must be at least 18 years old' 525: } 526: 527: =item * C<nullable> 528: 529: Like optional, 530: though this cannot be a coderef, 531: only a flag. 532: 533: =item * C<schema> 534: 535: You can validate nested hashrefs and arrayrefs using the C<schema> property: 536: 537: my $schema = { 538: user => { # 'user' is a hashref 539: type => 'hashref', 540: schema => { # Specify what the elements of the hash should be 541: name => { type => 'string' }, 542: age => { type => 'integer', min => 0 }, 543: hobbies => { # 'hobbies' is an array ref that this user has 544: type => 'arrayref', 545: schema => { type => 'string' }, # Validate each hobby 546: min => 1 # At least one hobby 547: } 548: } 549: }, metadata => { 550: type => 'hashref', 551: schema => { 552: created => { type => 'string' }, 553: tags => { 554: type => 'arrayref', 555: schema => { 556: type => 'string', 557: matches => qr/^[a-z]+$/ # Or you can say matches => '^[a-z]+$' 558: } 559: } 560: } 561: } 562: }; 563: 564: =item * C<validate> 565: 566: A snippet of code that validates the input. 567: It's passed the input arguments, 568: and return a string containing a reason for rejection, 569: or undef if it's allowed. 570: 571: my $schema = { 572: user => { 573: type => 'string', 574: validate => sub { 575: if($_[0]->{'password'} eq 'bar') { 576: return undef; 577: } 578: return 'Invalid password, try again'; 579: } 580: }, password => { 581: type => 'string' 582: } 583: }; 584: 585: =item * C<transform> 586: 587: A code reference to a subroutine that transforms/sanitizes the parameter value before validation. 588: The subroutine should accept the parameter value as an argument and return the transformed value. 589: The transformation is applied before any validation rules are checked, allowing you to normalize 590: or clean data before it is validated. 591: 592: Common use cases include trimming whitespace, normalizing case, formatting phone numbers, 593: sanitizing user input, and converting between data formats. 594: 595: # Simple string transformations 596: username => { 597: type => 'string', 598: transform => sub { lc(trim($_[0])) }, # lowercase and trim 599: matches => qr/^[a-z0-9_]+$/ 600: } 601: 602: email => { 603: type => 'string', 604: transform => sub { lc(trim($_[0])) }, # normalize email 605: matches => qr/^[\w\.\-]+@[\w\.\-]+\.\w+$/ 606: } 607: 608: # Array transformations 609: tags => { 610: type => 'arrayref', 611: transform => sub { [map { lc($_) } @{$_[0]}] }, # lowercase all elements 612: element_type => 'string' 613: } 614: 615: keywords => { 616: type => 'arrayref', 617: transform => sub { 618: my @arr = map { lc(trim($_)) } @{$_[0]}; 619: my %seen; 620: return [grep { !$seen{$_}++ } @arr]; # remove duplicates 621: } 622: } 623: 624: # Numeric transformations 625: quantity => { 626: type => 'integer', 627: transform => sub { int($_[0] + 0.5) }, # round to nearest integer 628: min => 1 629: } 630: 631: # Sanitization 632: slug => { 633: type => 'string', 634: transform => sub { 635: my $str = lc(trim($_[0])); 636: $str =~ s/[^\w\s-]//g; # remove special characters 637: $str =~ s/\s+/-/g; # replace spaces with hyphens 638: return $str; 639: }, 640: matches => qr/^[a-z0-9-]+$/ 641: } 642: 643: phone => { 644: type => 'string', 645: transform => sub { 646: my $str = $_[0]; 647: $str =~ s/\D//g; # remove all non-digits 648: return $str; 649: }, 650: matches => qr/^\d{10}$/ 651: } 652: 653: The C<transform> function is applied to the value before any validation checks (C<min>/C<minimum>, C<max>, 654: C<matches>, C<callback>, etc.), ensuring that validation rules are checked against the cleaned data. 655: 656: Transformations work with all parameter types including nested structures: 657: 658: user => { 659: type => 'hashref', 660: schema => { 661: name => { 662: type => 'string', 663: transform => sub { trim($_[0]) } 664: }, email => { 665: type => 'string', 666: transform => sub { lc(trim($_[0])) } 667: } 668: } 669: } 670: 671: Transformations can also be defined in custom types for reusability: 672: 673: my $custom_types = { 674: email => { 675: type => 'string', 676: transform => sub { lc(trim($_[0])) }, 677: matches => qr/^[\w\.\-]+@[\w\.\-]+\.\w+$/ 678: } 679: }; 680: 681: Note that the transformed value is what gets returned in the validated result and is what 682: subsequent validation rules will check against. If a transformation might fail, ensure it 683: handles edge cases appropriately. 684: It is the responsibility of the transformer to ensure that the type of the returned value is correct, 685: since that is what will be validated. 686: 687: Many validators also allow a code ref to be passed so that you can create your own, conditional validation rule, e.g.: 688: 689: $schema = { 690: age => { 691: type => 'integer', 692: min => sub { 693: my ($value, $all_params) = @_; 694: return $all_params->{country} eq 'US' ? 21 : 18; 695: } 696: } 697: } 698: 699: =item * C<validator> 700: 701: A synonym of C<validate>, for compatibility with L<Data::Processor>. 702: 703: =item * C<cross_validation> 704: 705: A reference to a hash that defines validation rules that depend on more than one parameter. 706: Cross-field validations are performed after all individual parameter validations have passed, 707: allowing you to enforce business logic that requires checking relationships between different fields. 708: 709: Each cross-validation rule is a key-value pair where the key is a descriptive name for the validation 710: and the value is a code reference that accepts a hash reference of all validated parameters. 711: The subroutine should return C<undef> if the validation passes, or an error message string if it fails. 712: 713: my $schema = { 714: password => { type => 'string', min => 8 }, 715: password_confirm => { type => 'string' } 716: }; 717: 718: my $cross_validation = { 719: passwords_match => sub { 720: my $params = shift; 721: return $params->{password} eq $params->{password_confirm} 722: ? undef : "Passwords don't match"; 723: } 724: }; 725: 726: my $validated = validate_strict( 727: schema => $schema, 728: input => $input, 729: cross_validation => $cross_validation 730: ); 731: 732: Common use cases include password confirmation, date range validation, numeric comparisons, 733: and conditional requirements: 734: 735: # Date range validation 736: my $cross_validation = { 737: date_range_valid => sub { 738: my $params = shift; 739: return $params->{start_date} le $params->{end_date} 740: ? undef : "Start date must be before or equal to end date"; 741: } 742: }; 743: 744: # Price range validation 745: my $cross_validation = { 746: price_range_valid => sub { 747: my $params = shift; 748: return $params->{min_price} <= $params->{max_price} 749: ? undef : "Minimum price must be less than or equal to maximum price"; 750: } 751: }; 752: 753: # Conditional required field 754: my $cross_validation = { 755: address_required_for_delivery => sub { 756: my $params = shift; 757: if ($params->{shipping_method} eq 'delivery' && !$params->{delivery_address}) { 758: return "Delivery address is required when shipping method is 'delivery'"; 759: } 760: return undef; 761: } 762: }; 763: 764: Multiple cross-validations can be defined in the same hash, and they are all checked in order. 765: If any cross-validation fails, the function will C<croak> with the error message returned by the validation: 766: 767: my $cross_validation = { 768: passwords_match => sub { 769: my $params = shift; 770: return $params->{password} eq $params->{password_confirm} 771: ? undef : "Passwords don't match"; 772: }, 773: emails_match => sub { 774: my $params = shift; 775: return $params->{email} eq $params->{email_confirm} 776: ? undef : "Email addresses don't match"; 777: }, 778: age_matches_birth_year => sub { 779: my $params = shift; 780: my $current_year = (localtime)[5] + 1900; 781: my $calculated_age = $current_year - $params->{birth_year}; 782: return abs($calculated_age - $params->{age}) <= 1 783: ? undef : "Age doesn't match birth year"; 784: } 785: }; 786: 787: Cross-validations receive the parameters after individual validation and transformation have been applied, 788: so you can rely on the data being in the correct format and type: 789: 790: my $schema = { 791: email => { 792: type => 'string', 793: transform => sub { lc($_[0]) } # Lowercased before cross-validation 794: }, 795: email_confirm => { 796: type => 'string', 797: transform => sub { lc($_[0]) } 798: } 799: }; 800: 801: my $cross_validation = { 802: emails_match => sub { 803: my $params = shift; 804: # Both emails are already lowercased at this point 805: return $params->{email} eq $params->{email_confirm} 806: ? undef : "Email addresses don't match"; 807: } 808: }; 809: 810: Cross-validations can access nested structures and optional fields: 811: 812: my $cross_validation = { 813: guardian_required_for_minors => sub { 814: my $params = shift; 815: if ($params->{user}{age} < 18 && !$params->{guardian}) { 816: return "Guardian information required for users under 18"; 817: } 818: return undef; 819: } 820: }; 821: 822: =item * metadata 823: 824: Fields starting with <_> are generated by L<App::Test::Generator::SchemaExtractor>, 825: and are currently ignored. 826: 827: =item * schematic 828: 829: TODO: gives an idea of what the field will be, e.g. C<filename>. 830: 831: All cross-validations must pass for the overall validation to succeed. 832: 833: =item * C<relationships> 834: 835: A reference to an array that defines validation rules based on relationships between parameters. 836: Relationship validations are performed after all individual parameter validations have passed, 837: but before cross-validations. 838: 839: Each relationship is a hash reference with a C<type> field and additional fields depending on the type: 840: 841: =over 4 842: 843: =item * B<mutually_exclusive> 844: 845: Parameters that cannot be specified together. 846: 847: relationships => [ 848: { 849: type => 'mutually_exclusive', 850: params => ['file', 'content'], 851: description => 'Cannot specify both file and content' 852: } 853: ] 854: 855: =item * B<required_group> 856: 857: At least one parameter from the group must be specified. 858: 859: relationships => [ 860: { 861: type => 'required_group', 862: params => ['id', 'name'], 863: logic => 'or', 864: description => 'Must specify either id or name' 865: } 866: ] 867: 868: =item * B<conditional_requirement> 869: 870: If one parameter is specified, another becomes required. 871: 872: relationships => [ 873: { 874: type => 'conditional_requirement', 875: if => 'async', 876: then_required => 'callback', 877: description => 'When async is specified, callback is required' 878: } 879: ] 880: 881: =item * B<dependency> 882: 883: One parameter requires another to be present. 884: 885: relationships => [ 886: { 887: type => 'dependency', 888: param => 'port', 889: requires => 'host', 890: description => 'port requires host to be specified' 891: } 892: ] 893: 894: =item * B<value_constraint> 895: 896: Specific value requirements between parameters. 897: 898: relationships => [ 899: { 900: type => 'value_constraint', 901: if => 'ssl', 902: then => 'port', 903: operator => '==', 904: value => 443, 905: description => 'When ssl is specified, port must equal 443' 906: } 907: ] 908: 909: =item * B<value_conditional> 910: 911: Parameter required when another has a specific value. 912: 913: relationships => [ 914: { 915: type => 'value_conditional', 916: if => 'mode', 917: equals => 'secure', 918: then_required => 'key', 919: description => "When mode equals 'secure', key is required" 920: } 921: ] 922: 923: =back 924: 925: If a parameter is optional and its value is C<undef>, 926: validation will be skipped for that parameter. 927: 928: If the validation fails, the function will C<croak> with an error message describing the validation failure. 929: 930: 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. 931: 932: The C<description> field is optional but recommended for clearer error messages. 933: 934: =back 935: 936: =head2 Example Usage 937: 938: my $schema = { 939: host => { type => 'string' }, 940: port => { type => 'integer' }, 941: ssl => { type => 'boolean' }, 942: file => { type => 'string', optional => 1 }, 943: content => { type => 'string', optional => 1 } 944: }; 945: 946: my $relationships = [ 947: { 948: type => 'mutually_exclusive', 949: params => ['file', 'content'] 950: }, 951: { 952: type => 'required_group', 953: params => ['host', 'file'] 954: }, 955: { 956: type => 'dependency', 957: param => 'port', 958: requires => 'host' 959: }, 960: { 961: type => 'value_constraint', 962: if => 'ssl', 963: then => 'port', 964: operator => '==', 965: value => 443 966: } 967: ]; 968: 969: my $validated = validate_strict( 970: schema => $schema, 971: input => $input, 972: relationships => $relationships 973: ); 974: 975: =head1 MIGRATION FROM LEGACY VALIDATORS 976: 977: =head2 From L<Params::Validate> 978: 979: # Old style 980: validate(@_, { 981: name => { type => SCALAR }, 982: age => { type => SCALAR, regex => qr/^\d+$/ } 983: }); 984: 985: # New style 986: validate_strict( 987: schema => { # or "members" 988: name => 'string', 989: age => { type => 'integer', min => 0 } 990: }, 991: args => { @_ } 992: ); 993: 994: =head2 From L<Type::Params> 995: 996: # Old style 997: my ($name, $age) = validate_positional \@_, Str, Int; 998: 999: # New style - requires converting to named parameters first 1000: my %args = (name => $_[0], age => $_[1]); 1001: my $validated = validate_strict( 1002: schema => { name => 'string', age => 'integer' }, 1003: args => \%args 1004: ); 1005: 1006: =cut 1007: 1008: sub validate_strict 1009: { ●1010 → 1017 → 1025 1010: my $params = Params::Get::get_params(undef, \@_); 1011: 1012: my $schema = $params->{'schema'} || $params->{'members'}; 1013: my $args = $params->{'args'} || $params->{'input'}; 1014: my $logger = $params->{'logger'}; 1015: my $custom_types = $params->{'custom_types'}; 1016: my $unknown_parameter_handler = $params->{'unknown_parameter_handler'}; 1017: if(!defined($unknown_parameter_handler)) {Mutants (Total: 1, Killed: 1, Survived: 0)
1018: if($params->{'carp_on_warn'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1019: $unknown_parameter_handler = 'warn'; 1020: } else { 1021: $unknown_parameter_handler = 'die'; 1022: } 1023: } 1024: ●1025 → 1029 → 1034 1025: return $args if(!defined($schema)); # No schema, allow all arguments
Mutants (Total: 2, Killed: 2, Survived: 0)
1026: 1027: # Accept arrayref schema: [{ name=>'param', type=>'...', ... }, ...] 1028: # Normalise to the standard named-parameter hashref form before further processing. 1029: if(ref($schema) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1030: $schema = _schema_from_arrayref($schema, $logger); 1031: } 1032: 1033: # Check if schema and args are references to hashes ●1034 → 1034 → 1039 1034: if(ref($schema) ne 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1035: _error($logger, 'validate_strict: schema must be a hash reference'); 1036: } 1037: 1038: # Inspired by Data::Processor ●1039 → 1042 → 1052 1039: my $schema_description = $params->{'description'} || 'validate_strict'; 1040: my $error_msg = $params->{'error_msg'}; 1041: 1042: if($schema->{'members'} && ($schema->{'description'} || $schema->{'error_msg'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1043: $schema_description = $schema->{'description'}; 1044: $error_msg = $schema->{'error_msg'}; 1045: $schema = $schema->{'members'}; 1046: # The members value may also be in arrayref form 1047: if(ref($schema) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1048: $schema = _schema_from_arrayref($schema, $logger); 1049: } 1050: } 1051: ●1052 → 1052 → 1058 1052: if(exists($params->{'args'}) && (!defined($args))) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1053: $args = {}; 1054: } elsif((ref($args) ne 'HASH') && (ref($args) ne 'ARRAY')) { 1055: _error($logger, $error_msg || "$schema_description: args must be a hash or array reference"); 1056: } 1057: ●1058 → 1058 → 1080 1058: if(ref($args) eq 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1059: # Named args 1060: foreach my $key (keys %{$args}) { 1061: if(!exists($schema->{$key})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1062: if($unknown_parameter_handler eq 'die') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1063: _error($logger, "$schema_description: Unknown parameter '$key'"); 1064: } elsif($unknown_parameter_handler eq 'warn') { 1065: _warn($logger, "$schema_description: Unknown parameter '$key'"); 1066: next; 1067: } elsif($unknown_parameter_handler eq 'ignore') { 1068: if($logger) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1069: $logger->debug(__PACKAGE__ . ": $schema_description: Unknown parameter '$key'"); 1070: } 1071: next; 1072: } else { 1073: _error($logger, "$schema_description: '$unknown_parameter_handler' unknown_parameter_handler must be one of die, warn, ignore"); 1074: } 1075: } 1076: } 1077: } 1078: 1079: # Find out if this routine takes positional arguments ●1080 → 1081 → 1102 1080: my $are_positional_args = -1; 1081: foreach my $key (keys %{$schema}) { 1082: if(defined(my $rules = $schema->{$key})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1083: if(ref($rules) eq 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1084: if(!defined($rules->{'position'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1085: if($are_positional_args == 1) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1086: _error($logger, "::validate_strict: $key is missing position value"); 1087: } 1088: $are_positional_args = 0; 1089: last; 1090: } 1091: $are_positional_args = 1; 1092: } else { 1093: $are_positional_args = 0; 1094: last; 1095: } 1096: } else { 1097: $are_positional_args = 0; 1098: last; 1099: } 1100: } 1101: ●1102 → 1104 → 1911 1102: my %validated_args; 1103: my %invalid_args; 1104: foreach my $key (keys %{$schema}) { 1105: my $rules = $schema->{$key}; 1106: my $value; 1107: if($are_positional_args == 1) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1108: if(ref($args) ne 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1109: _error($logger, "::validate_strict: position $rules->{position} given for '$key', but args isn't an array"); 1110: } 1111: $value = @{$args}[$rules->{'position'}]; 1112: } else { 1113: $value = $args->{$key}; 1114: } 1115: 1116: if(!defined($rules)) { # Allow anything
Mutants (Total: 1, Killed: 1, Survived: 0)
1117: $validated_args{$key} = $value; 1118: next; 1119: } 1120: 1121: # If rules are a simple type string 1122: if(ref($rules) eq '') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1123: $rules = { type => $rules }; 1124: } 1125: 1126: my $is_optional = 0; 1127: 1128: my $rule_description = $schema_description; # Can be overridden in each element 1129: 1130: if(ref($rules) eq 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1131: if(exists($rules->{'description'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1132: $rule_description = $rules->{'description'}; 1133: } 1134: # For stringref: validate and dereference before transform so that 1135: # transform (and all subsequent rule handlers) see the plain string. 1136: # Preserve the original ref so optional => CODE receives what the caller passed. 1137: my $pre_deref_value = $value; 1138: my $is_stringref_type = defined($value) && defined($rules->{'type'}) && !ref($rules->{'type'}) && lc($rules->{'type'}) eq 'stringref'; 1139: if($is_stringref_type) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1140: if(ref($value) ne 'SCALAR') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1141: my $got = ref($value) ? 'a ' . ref($value) . ' reference' : 'a plain scalar'; 1142: _error($logger, $rules->{'error_msg'} || "$rule_description: Parameter '$key' must be a string reference, not $got"); 1143: } 1144: $value = ${$value}; 1145: } 1146: if($rules->{'transform'} && defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1147: if(ref($rules->{'transform'}) eq 'CODE') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1148: $value = &{$rules->{'transform'}}($value); 1149: } else { 1150: _error($logger, "$rule_description: transforms must be a code ref"); 1151: } 1152: } 1153: if(exists($rules->{optional})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1154: if(ref($rules->{'optional'}) eq 'CODE') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1155: # For stringref the coderef receives the original SCALAR ref (what 1156: # the caller supplied), not the internally-dereferenced plain string. 1157: # For all other types the post-transform value is passed, as before. 1158: my $opt_arg = $is_stringref_type ? $pre_deref_value : $value; 1159: $is_optional = &{$rules->{optional}}($opt_arg, $args); 1160: } else { 1161: $is_optional = $rules->{'optional'}; 1162: } 1163: } elsif($rules->{nullable}) { 1164: $is_optional = $rules->{'nullable'}; 1165: } 1166: } 1167: 1168: # Handle optional parameters 1169: if((ref($rules) eq 'HASH') && $is_optional) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1170: my $look_for_default = 0; 1171: if($are_positional_args == 1) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1172: # if(!defined(@{$args}[$rules->{'position'}])) { 1173: if(!defined($args->[$rules->{position}])) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1174: $look_for_default = 1; 1175: } 1176: } else { 1177: if(!exists($args->{$key})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1178: $look_for_default = 1; 1179: } 1180: } 1181: if($look_for_default) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1182: if($are_positional_args == 1) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1183: if(scalar(@{$args}) < $rules->{'position'}) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1184: # arg array is too short, so it must be missing 1185: _error($logger, "$rule_description: Required parameter '$key' is missing"); 1186: next; 1187: } 1188: } 1189: if(exists($rules->{'default'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1190: # Populate missing optional parameters with the specified output values 1191: $validated_args{$key} = $rules->{'default'}; 1192: next; # default wins; do not fall through to the schema branch 1193: } 1194: 1195: if($rules->{'schema'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1196: $value = _apply_nested_defaults({}, $rules->{'schema'}); 1197: next unless scalar(%{$value}); 1198: # The nested schema has a default value 1199: } else { 1200: next; # optional and missing 1201: } 1202: } 1203: } elsif((ref($args) eq 'HASH') && !exists($args->{$key})) { 1204: # The parameter is required 1205: # Use exists rather than defined, so that an undefined value can be passed, but the key is there 1206: _error($logger, "$rule_description: Required parameter '$key' is missing"); 1207: } 1208: 1209: # Normalise union type shorthand: { type => ['string', 'integer'], ... } 1210: # into the array-of-rules form that the ARRAY handler below already supports. 1211: # Each candidate type inherits all other constraints from the parent rule 1212: # (min, max, matches, optional, etc.) so they are each fully validated. 1213: # Must run after optional/transform handling above but before rule dispatch below. 1214: if(ref($rules) eq 'HASH' && ref($rules->{'type'}) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1215: my %base = %{$rules}; 1216: my @type_list = @{delete $base{'type'}}; 1217: if(!@type_list) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1218: _error($logger, "$rule_description: Parameter '$key': union type list must not be empty"); 1219: } 1220: # Expand into one full rule hash per candidate type 1221: $rules = [ map { { %base, type => $_ } } @type_list ]; 1222: } 1223: 1224: # Validate based on rules 1225: if(ref($rules) eq 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1226: if(defined(my $min = $rules->{'min'} // $rules->{'minimum'}) && defined(my $max = $rules->{'max'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1227: if($min > $max) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1228: _error($logger, "validate_strict($key): min must be <= max ($min > $max)"); 1229: } 1230: } 1231: 1232: # memberof and its synonym enum cannot be combined with min or max 1233: if($rules->{'memberof'} || $rules->{'enum'} || $rules->{'values'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1234: if(defined(my $min = $rules->{'min'} // $rules->{'minimum'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1235: _error($logger, "validate_strict($key): min ($min) makes no sense with memberof/enum/values"); 1236: } 1237: if(defined(my $max = $rules->{'max'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1238: _error($logger, "validate_strict($key): max ($max) makes no sense with memberof/enum/values"); 1239: } 1240: } 1241: 1242: foreach my $rule_name (keys %$rules) { 1243: my $rule_value = $rules->{$rule_name}; 1244: 1245: if((ref($rule_value) eq 'CODE')
Mutants (Total: 1, Killed: 1, Survived: 0)
1246: && ($rule_name ne 'validate') 1247: && ($rule_name ne 'callback') 1248: && ($rule_name ne 'validator') 1249: && ($rule_name ne 'transform') # already applied before this loop 1250: && ($rule_name ne 'optional')) { # already applied before this loop 1251: $rule_value = &{$rule_value}($value, $args); 1252: } 1253: 1254: # Better OOP, the routine has been given an object rather than a scalar 1255: if(Scalar::Util::blessed($rule_value) && $rule_value->can('as_string')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1256: $rule_value = $rule_value->as_string(); 1257: } 1258: 1259: if($rule_name eq 'type') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1260: my $type = lc($rule_value); 1261: 1262: if(($type eq 'string') || ($type eq 'str')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1263: if(ref($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1264: _error($logger, $rules->{'error_msg'} || "$rule_description: Parameter '$key' must be a string"); 1265: } 1266: unless((ref($value) eq '') || (defined($value) && length($value))) { # Allow undef for optional strings
Mutants (Total: 1, Killed: 1, Survived: 0)
1267: _error($logger, $rules->{'error_msg'} || "$rule_description: Parameter '$key' must be a string"); 1268: } 1269: } elsif(($type eq 'integer') || ($type eq 'int')) { 1270: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1271: next; # Skip if number is undefined 1272: } 1273: if(!Scalar::Util::looks_like_number($value) || ($value - $value) != 0 || $value != int($value)) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1274: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1275: _error($logger, $rules->{'error_msg'}); 1276: } else { 1277: _error($logger, "$rule_description: Parameter '$key' ($value) must be an integer"); 1278: } 1279: } 1280: $value = int($value); # Coerce to integer 1281: } elsif(($type eq 'number') || ($type eq 'float') || ($type eq 'num') || ($type eq 'double')) { 1282: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1283: next; # Skip if number is undefined 1284: } 1285: if(!Scalar::Util::looks_like_number($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1286: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1287: _error($logger, $rules->{'error_msg'}); 1288: } else { 1289: _error($logger, "$rule_description: Parameter '$key' must be a number"); 1290: } 1291: } 1292: # $value = eval $value; # Coerce to number (be careful with eval) 1293: $value = 0 + $value; # Numeric coercion 1294: } elsif($type eq 'arrayref') { 1295: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1296: next; # Skip if arrayref is undefined 1297: } 1298: if(ref($value) ne 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1299: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1300: _error($logger, $rules->{'error_msg'}); 1301: } else { 1302: _error($logger, "$rule_description: Parameter '$key' must be an arrayref, not " . ref($value)); 1303: } 1304: } 1305: } elsif($type eq 'hashref') { 1306: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1307: next; # Skip if hashref is undefined 1308: } 1309: if(ref($value) ne 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1310: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1311: _error($logger, $rules->{'error_msg'}); 1312: } else { 1313: _error($logger, "$rule_description: Parameter '$key' must be an hashref"); 1314: } 1315: } 1316: } elsif($type eq 'scalar') { 1317: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1318: next; # Skip if undefined 1319: } 1320: if(ref($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1321: _error($logger, $rules->{'error_msg'} || "$rule_description: Parameter '$key' must be a scalar, not a " . ref($value) . ' reference'); 1322: } 1323: } elsif($type eq 'scalarref') { 1324: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1325: next; # Skip if undefined 1326: } 1327: if(ref($value) ne 'SCALAR') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1328: my $got = ref($value) ? 'a ' . ref($value) . ' reference' : 'a plain scalar'; 1329: _error($logger, $rules->{'error_msg'} || "$rule_description: Parameter '$key' must be a scalar reference, not $got"); 1330: } 1331: } elsif($type eq 'stringref') { 1332: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1333: next; # Skip if undefined 1334: } 1335: # The early-deref block validated the SCALAR ref and set $value to the 1336: # plain string. If transform subsequently returned a reference, reject it. 1337: if(ref($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1338: _error($logger, $rules->{'error_msg'} || "$rule_description: Parameter '$key' stringref transform must return a plain string, not a " . ref($value) . ' reference'); 1339: } 1340: } elsif(($type eq 'boolean') || ($type eq 'bool')) { 1341: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1342: next; # Skip if bool is undefined 1343: } 1344: if(defined(my $b = $Readonly::Values::Boolean::booleans{$value})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1345: $value = $b; 1346: } else { 1347: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1348: _error($logger, $rules->{'error_msg'}); 1349: } else { 1350: _error($logger, "$rule_description: Parameter '$key' ($value) must be a boolean"); 1351: } 1352: } 1353: } elsif($type eq 'coderef') { 1354: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1355: next; # Skip if code is undefined 1356: } 1357: if(ref($value) ne 'CODE') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1358: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1359: _error($logger, $rules->{'error_msg'}); 1360: } else { 1361: _error($logger, "$rule_description: Parameter '$key' must be a coderef, not a ref to " . ref($value)); 1362: } 1363: } 1364: } elsif($type eq 'object') { 1365: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1366: next; # Skip if object is undefined 1367: } 1368: if(!Scalar::Util::blessed($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1369: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1370: _error($logger, $rules->{'error_msg'}); 1371: } else { 1372: _error($logger, "$rule_description: Parameter '$key' must be an object"); 1373: } 1374: } 1375: } elsif(my $custom_type = $custom_types->{$type}) { 1376: if($custom_type->{'transform'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1377: # The custom type has a transform embedded within it 1378: if(ref($custom_type->{'transform'}) eq 'CODE') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1379: $value = &{$custom_type->{'transform'}}($value); 1380: } else { 1381: _error($logger, "$rule_description: transforms must be a code ref"); 1382: next; 1383: } 1384: } 1385: validate_strict({ input => { $key => $value }, schema => { $key => $custom_type }, custom_types => $custom_types }); 1386: } else { 1387: _error($logger, "$rule_description: Unknown type '$type'"); 1388: } 1389: } elsif(($rule_name eq 'min') || ($rule_name eq 'minimum')) { 1390: if(!defined($rules->{'type'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1391: _error($logger, "$rule_description: Don't know type of '$key' to determine its minimum value $rule_value"); 1392: } 1393: my $type = lc($rules->{'type'}); 1394: if(exists($custom_types->{$type}->{'min'}) || exists($custom_types->{$type}->{minimum})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1395: $rule_value = $custom_types->{$type}->{'min'} // $custom_types->{$type}->{minumum}; 1396: $type = $custom_types->{$type}->{'type'}; 1397: } 1398: if(($type eq 'string') || ($type eq 'str') || ($type eq 'stringref')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1399: if($rule_value < 0) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1400: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1401: _error($logger, $rules->{'error_msg'}); 1402: } else { 1403: _error($logger, "$rule_description: String parameter '$key' has meaningless minimum value that is less than zero"); 1404: } 1405: } 1406: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1407: next; # Skip if string is undefined 1408: } 1409: if(defined(my $len = _number_of_characters($value))) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1410: if($len < $rule_value) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1411: _error($logger, $rules->{'error_msg'} || "$rule_description: String parameter '$key' too short, ($len characters), must be at least $rule_value characters"); 1412: $invalid_args{$key} = 1; 1413: } 1414: } else { 1415: _error($logger, $rules->{'error_msg'} || "$rule_description: '$key' can't be decoded"); 1416: $invalid_args{$key} = 1; 1417: } 1418: } elsif($type eq 'arrayref') { 1419: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1420: next; # Skip if array is undefined 1421: } 1422: if(ref($value) ne 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1423: if($rules->{'error_msg'}) {
1424: _error($logger, $rules->{'error_msg'}); 1425: } else { 1426: _error($logger, "$rule_description: Parameter '$key' must be an arrayref, not " . ref($value)); 1427: } 1428: } 1429: if(scalar(@{$value}) < $rule_value) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_1423_8: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 4, Killed: 4, Survived: 0)
1430: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1431: _error($logger, $rules->{'error_msg'}); 1432: } else { 1433: _error($logger, "$rule_description: Parameter '$key' must be at least length $rule_value"); 1434: } 1435: $invalid_args{$key} = 1; 1436: } 1437: } elsif($type eq 'hashref') { 1438: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1439: next; # Skip if hash is undefined 1440: } 1441: if(scalar(keys(%{$value})) < $rule_value) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1442: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1443: _error($logger, $rules->{'error_msg'}); 1444: } else { 1445: _error($logger, "$rule_description: Parameter '$key' must contain at least $rule_value keys"); 1446: } 1447: $invalid_args{$key} = 1; 1448: } 1449: } elsif(($type eq 'integer') || ($type eq 'number') || ($type eq 'float')) { 1450: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1451: next; # Skip if hash is undefined 1452: } 1453: if(Scalar::Util::looks_like_number($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1454: if($value < $rule_value) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1455: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1456: _error($logger, $rules->{'error_msg'}); 1457: } elsif(($type eq 'integer') && ($value == 0)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1458: _error($logger, "$rule_description: Parameter '$key' ($value) must be a positive number"); 1459: } elsif(($type eq 'integer') && ($value == 1)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1460: _error($logger, "$rule_description: Parameter '$key' ($value) must be a positive, non-zero number"); 1461: } else { 1462: _error($logger, "$rule_description: Parameter '$key' ($value) must be at least $rule_value"); 1463: } 1464: $invalid_args{$key} = 1; 1465: next; 1466: } 1467: } else { 1468: if($rules->{'error_msg'}) {
1469: _error($logger, $rules->{'error_msg'}); 1470: } else { 1471: _error($logger, "$rule_description: Parameter '$key' ($value) must be a number"); 1472: } 1473: next; 1474: } 1475: } else { 1476: _error($logger, "$rule_description: Parameter '$key' of type '$type' has meaningless min value $rule_value"); 1477: } 1478: } elsif($rule_name eq 'max') { 1479: if(!defined($rules->{'type'})) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_1468_8: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
1480: _error($logger, "$rule_description: Don't know type of '$key' to determine its maximum value $rule_value"); 1481: } 1482: my $type = lc($rules->{'type'}); 1483: if(exists($custom_types->{$type}->{'max'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1484: $rule_value = $custom_types->{$type}->{'max'}; 1485: $type = $custom_types->{$type}->{'type'}; 1486: } 1487: if(($type eq 'string') || ($type eq 'str') || ($type eq 'stringref')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1488: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1489: next; # Skip if string is undefined 1490: } 1491: if(defined(my $len = _number_of_characters($value))) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1492: if($len > $rule_value) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1493: _error($logger, $rules->{'error_msg'} || "$rule_description: String parameter '$key' too long, ($len characters), must be no longer than $rule_value"); 1494: $invalid_args{$key} = 1; 1495: } 1496: } else { 1497: _error($logger, $rules->{'error_msg'} || "$rule_description: '$key' can't be decoded"); 1498: $invalid_args{$key} = 1; 1499: } 1500: } elsif($type eq 'arrayref') { 1501: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1502: next; # Skip if string is undefined 1503: } 1504: if(ref($value) ne 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1505: if($rules->{'error_msg'}) {
1506: _error($logger, $rules->{'error_msg'}); 1507: } else { 1508: _error($logger, "$rule_description: Parameter '$key' must be an arrayref, not " . ref($value)); 1509: } 1510: } 1511: if(scalar(@{$value}) > $rule_value) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_1505_8: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 4, Killed: 4, Survived: 0)
1512: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1513: _error($logger, $rules->{'error_msg'}); 1514: } else { 1515: _error($logger, "$rule_description: Parameter '$key' must contain no more than $rule_value items"); 1516: } 1517: $invalid_args{$key} = 1; 1518: } 1519: } elsif($type eq 'hashref') { 1520: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1521: next; # Skip if hash is undefined 1522: } 1523: if(scalar(keys(%{$value})) > $rule_value) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1524: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1525: _error($logger, $rules->{'error_msg'}); 1526: } else { 1527: _error($logger, "$rule_description: Parameter '$key' must contain no more than $rule_value keys"); 1528: } 1529: $invalid_args{$key} = 1; 1530: } 1531: } elsif(($type eq 'integer') || ($type eq 'number') || ($type eq 'float')) { 1532: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1533: next; # Skip if hash is undefined 1534: } 1535: if(Scalar::Util::looks_like_number($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1536: if($value > $rule_value) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1537: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1538: _error($logger, $rules->{'error_msg'}); 1539: } elsif(($type eq 'integer') && ($value == 0)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1540: _error($logger, "$rule_description: Parameter '$key' ($value) must be a negative number"); 1541: } elsif(($type eq 'integer') && ($value == -1)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1542: _error($logger, "$rule_description: Parameter '$key' ($value) must be a negative, non-zero number"); 1543: } else { 1544: _error($logger, "$rule_description: Parameter '$key' ($value) must be no more than $rule_value"); 1545: } 1546: $invalid_args{$key} = 1; 1547: next; 1548: } 1549: } else { 1550: if($rules->{'error_msg'}) {
1551: _error($logger, $rules->{'error_msg'}); 1552: } else { 1553: _error($logger, "$rule_description: Parameter '$key' ($value) must be a number"); 1554: } 1555: next; 1556: } 1557: } else { 1558: _error($logger, "$rule_description: Parameter '$key' of type '$type' has meaningless max value $rule_value"); 1559: } 1560: } elsif(($rule_name eq 'matches') || ($rule_name eq 'regex')) { 1561: if(!defined($value)) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_1550_8: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
1562: next; # Skip if string is undefined 1563: } 1564: eval { 1565: my $re = (ref($rule_value) eq 'Regexp') ? $rule_value : qr/\Q$rule_value\E/; 1566: if(($rules->{'type'} eq 'arrayref') || ($rules->{'type'} eq 'ArrayRef')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1567: my @matches = grep { $_ =~ $re } @{$value}; 1568: if(scalar(@matches) != scalar(@{$value})) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1569: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1570: _error($logger, $rules->{'error_msg'}); 1571: } else { 1572: _error($logger, "$rule_description: All members of parameter '$key' [", join(', ', @{$value}), "] must match pattern '$rule_value'"); 1573: } 1574: } 1575: } elsif($value !~ $re) { 1576: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1577: _error($logger, $rules->{'error_msg'}); 1578: } else { 1579: _error($logger, "$rule_description: Parameter '$key' ($value) must match pattern '$re'"); 1580: } 1581: } 1582: 1; 1583: }; 1584: if($@) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1585: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1586: _error($logger, $rules->{'error_msg'}); 1587: } else { 1588: _error($logger, "$rule_description: Parameter '$key' regex '$rule_value' error: $@"); 1589: } 1590: $invalid_args{$key} = 1; 1591: } 1592: } elsif($rule_name eq 'nomatch') { 1593: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1594: next; # Skip if string is undefined 1595: } 1596: if(($rules->{'type'} eq 'arrayref') || ($rules->{'type'} eq 'ArrayRef')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1597: my @matches = grep { /$rule_value/ } @{$value}; 1598: if(scalar(@matches)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1599: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1600: _error($logger, $rules->{'error_msg'}); 1601: } else { 1602: _error($logger, "$rule_description: No member of parameter '$key' [", join(', ', @{$value}), "] must match pattern '$rule_value'"); 1603: } 1604: } 1605: } elsif($value =~ $rule_value) { 1606: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1607: _error($logger, $rules->{'error_msg'}); 1608: } else { 1609: _error($logger, "$rule_description: Parameter '$key' ($value) must not match pattern '$rule_value'"); 1610: } 1611: $invalid_args{$key} = 1; 1612: } 1613: } elsif(($rule_name eq 'memberof') || ($rule_name eq 'enum') || ($rule_name eq 'values')) { 1614: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1615: next; # Skip if string is undefined 1616: } 1617: if(ref($rule_value) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1618: my $ok = 1; 1619: if(($rules->{'type'} eq 'integer') || ($rules->{'type'} eq 'number') || ($rules->{'type'} eq 'float')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1620: unless(List::Util::any { $_ == $value } @{$rule_value}) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1621: $ok = 0; 1622: } 1623: } else { 1624: my $l = lc($value); 1625: unless(List::Util::any { (!defined($rules->{'case_sensitive'}) || ($rules->{'case_sensitive'} == 1)) ? $_ eq $value : lc($_) eq $l } @{$rule_value}) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1626: $ok = 0; 1627: } 1628: } 1629: 1630: if(!$ok) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1631: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1632: _error($logger, $rules->{'error_msg'}); 1633: } else { 1634: _error($logger, "$rule_description: Parameter '$key' ($value) must be one of ", join(', ', @{$rule_value})); 1635: } 1636: $invalid_args{$key} = 1; 1637: } 1638: } else { 1639: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1640: _error($logger, $rules->{'error_msg'}); 1641: } else { 1642: _error($logger, "$rule_description: Parameter '$key' rule ($rule_value) must be an array reference"); 1643: } 1644: } 1645: } elsif($rule_name eq 'notmemberof') { 1646: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1647: next; # Skip if string is undefined 1648: } 1649: if(ref($rule_value) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1650: my $ok = 1; 1651: if(($rules->{'type'} eq 'integer') || ($rules->{'type'} eq 'number') || ($rules->{'type'} eq 'float')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1652: if(List::Util::any { $_ == $value } @{$rule_value}) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1653: $ok = 0; 1654: } 1655: } else { 1656: my $l = lc($value); 1657: if(List::Util::any { (!defined($rules->{'case_sensitive'}) || ($rules->{'case_sensitive'} == 1)) ? $_ eq $value : lc($_) eq $l } @{$rule_value}) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1658: $ok = 0; 1659: } 1660: } 1661: 1662: if(!$ok) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1663: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1664: _error($logger, $rules->{'error_msg'}); 1665: } else { 1666: _error($logger, "$rule_description: Parameter '$key' ($value) must not be one of ", join(', ', @{$rule_value})); 1667: } 1668: $invalid_args{$key} = 1; 1669: } 1670: } else { 1671: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1672: _error($logger, $rules->{'error_msg'}); 1673: } else { 1674: _error($logger, "$rule_description: Parameter '$key' rule ($rule_value) must be an array reference"); 1675: } 1676: } 1677: } elsif($rule_name eq 'isa') { 1678: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1679: next; # Skip if object not given 1680: } 1681: if($rules->{'type'} eq 'object') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1682: if(!$value->isa($rule_value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1683: _error($logger, "$rule_description: Parameter '$key' must be a '$rule_value' object got a " . (ref($value) ? ref($value) : $value) . ' object instead'); 1684: $invalid_args{$key} = 1; 1685: } 1686: } else { 1687: _error($logger, "$rule_description: Parameter '$key' has meaningless isa value $rule_value"); 1688: } 1689: } elsif($rule_name eq 'can') { 1690: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1691: next; # Skip if object not given 1692: } 1693: if($rules->{'type'} eq 'object') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1694: if(ref($rule_value) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1695: # List of methods 1696: foreach my $method(@{$rule_value}) { 1697: if(!$value->can($method)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1698: _error($logger, "$rule_description: Parameter '$key' must be an object that understands the $method method"); 1699: $invalid_args{$key} = 1; 1700: } 1701: } 1702: } elsif(!ref($rule_value)) { 1703: if(!$value->can($rule_value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1704: _error($logger, "$rule_description: Parameter '$key' must be an object that understands the $rule_value method"); 1705: $invalid_args{$key} = 1; 1706: } 1707: } else { 1708: _error($logger, "$rule_description: 'can' rule for Parameter '$key must be either a scalar or an arrayref"); 1709: } 1710: } else { 1711: _error($logger, "$rule_description: Parameter '$key' has meaningless can value '$rule_value' for parameter type $rules->{type}"); 1712: } 1713: } elsif($rule_name eq 'element_type') { 1714: if(($rules->{'type'} eq 'arrayref') || ($rules->{'type'} eq 'ArrayRef')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1715: my $type = $rule_value; 1716: my $custom_type = $custom_types->{$rule_value}; 1717: if($custom_type && $custom_type->{'type'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1718: $type = $custom_type->{'type'}; 1719: } 1720: foreach my $member(@{$value}) { 1721: if($custom_type && $custom_type->{'transform'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1722: # The custom type has a transform embedded within it 1723: if(ref($custom_type->{'transform'}) eq 'CODE') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1724: $member = &{$custom_type->{'transform'}}($member); 1725: } else { 1726: _error($logger, "$rule_description: transforms must be a code ref"); 1727: last; 1728: } 1729: } 1730: if(($type eq 'string') || ($type eq 'Str')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1731: if(ref($member)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1732: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1733: _error($logger, $rules->{'error_msg'}); 1734: } else { 1735: _error($logger, "$key can only contain strings"); 1736: } 1737: $invalid_args{$key} = 1; 1738: } 1739: } elsif($type eq 'integer') { 1740: if(ref($member) || ($member =~ /\D/)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1741: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1742: _error($logger, $rules->{'error_msg'}); 1743: } else { 1744: _error($logger, "$key can only contain integers (found $member)"); 1745: } 1746: $invalid_args{$key} = 1; 1747: } 1748: } elsif(($type eq 'number') || ($rule_value eq 'float')) { 1749: if(ref($member) || ($member !~ /^[-+]?(\d*\.\d+|\d+\.?\d*)$/)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1750: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1751: _error($logger, $rules->{'error_msg'}); 1752: } else { 1753: _error($logger, "$key can only contain numbers (found $member)"); 1754: } 1755: $invalid_args{$key} = 1; 1756: } 1757: } else { 1758: _error($logger, "BUG: Add $type to element_type list"); 1759: } 1760: } 1761: } else { 1762: _error($logger, "$rule_description: Parameter '$key' has meaningless element_type value $rule_value"); 1763: } 1764: } elsif($rule_name eq 'optional') { 1765: # Already handled at the beginning of the loop 1766: } elsif($rule_name eq 'nullable') { 1767: # Already handled at the beginning of the loop (same as optional) 1768: } elsif($rule_name eq 'default') { 1769: # Handled earlier 1770: } elsif($rule_name eq 'error_msg') { 1771: # Handled inline 1772: } elsif($rule_name eq 'transform') { 1773: # Handled before the loop 1774: } elsif($rule_name eq 'case_sensitive') { 1775: # Handled inline 1776: } elsif($rule_name eq 'description') { 1777: # A la, Data::Processor 1778: } elsif($rule_name =~ /^_/) { 1779: # Ignore internal/metadata fields from schema extraction 1780: } elsif($rule_name eq 'semantic') { 1781: if($rule_value eq 'unix_timestamp') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1782: if($value < 0 || $value > 2147483647) {
Mutants (Total: 7, Killed: 7, Survived: 0)
1783: error($logger, 'Invalid Unix timestamp: $value'); 1784: } 1785: } else { 1786: _warn($logger, "semantic type $rule_value is not yet supported"); 1787: } 1788: } elsif($rule_name eq 'schema') { 1789: # Nested schema Run the given schema against each element of the array 1790: if(($rules->{'type'} eq 'arrayref') || ($rules->{'type'} eq 'ArrayRef')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1791: if(ref($value) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1792: foreach my $member(@{$value}) { 1793: # Distinguish two schema forms: 1794: # (a) Rule hash â has a top-level 'type' key, e.g. { type=>'string', matches=>qr/.../ } 1795: # â validate each element against that rule directly. 1796: # (b) Field-schema hash â keys are field names whose values are rule hashes, 1797: # e.g. { name=>{type=>'string'}, age=>{type=>'integer'} } 1798: # â validate each hashref element against the field schema directly. 1799: my $is_field_schema = (ref($rule_value) eq 'HASH') && !exists($rule_value->{'type'}); 1800: my %inner = (custom_types => $custom_types); 1801: if($is_field_schema) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1802: $inner{input} = $member; 1803: $inner{schema} = $rule_value; 1804: } else { 1805: $inner{input} = { $key => $member }; 1806: $inner{schema} = { $key => $rule_value }; 1807: } 1808: if(!validate_strict(\%inner)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1809: $invalid_args{$key} = 1; 1810: } 1811: } 1812: } elsif(defined($value)) { # Allow undef for optional values 1813: _error($logger, "$rule_description: nested schema: Parameter '$value' must be an arrayref"); 1814: } 1815: } elsif($rules->{'type'} eq 'hashref') { 1816: if(ref($rule_value) eq 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1817: # Apply nested defaults before validation 1818: my $nested_with_defaults = _apply_nested_defaults($value, $rule_value); 1819: if(scalar keys(%{$nested_with_defaults})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1820: if(my $new_args = validate_strict({ input => $nested_with_defaults, schema => $rule_value, custom_types => $custom_types })) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1821: $value = $new_args; 1822: } else { 1823: $invalid_args{$key} = 1; 1824: } 1825: } 1826: } else { 1827: _error($logger, "$rule_description: nested schema: Parameter '$value' must be an hashref"); 1828: } 1829: } else { 1830: _error($logger, "$rule_description: Parameter '$key': 'schema' only supports arrayref and hashref, not $rules->{type}"); 1831: } 1832: } elsif(($rule_name eq 'validate') || ($rule_name eq 'validator')) { 1833: if(ref($rule_value) eq 'CODE') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1834: if(my $error = &{$rule_value}($args)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1835: _error($logger, "$rule_description: $key not valid: $error"); 1836: $invalid_args{$key} = 1; 1837: } 1838: } else { 1839: # _error($logger, "$rule_description: Parameter '$key': 'validate' only supports coderef, not $value"); 1840: _error($logger, "$rule_description: Parameter '$key': 'validate' only supports coderef, not " . ref($rule_value) // $rule_value); 1841: } 1842: } elsif ($rule_name eq 'callback') { 1843: # Custom validation code 1844: unless (defined &$rule_value) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1845: _error($logger, "$rule_description: callback for '$key' must be a code reference"); 1846: } 1847: my $res = $rule_value->($value, $args, $schema); 1848: unless ($res) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1849: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1850: _error($logger, $rules->{'error_msg'}); 1851: } else { 1852: _error($logger, "$rule_description: Parameter '$key' failed custom validation"); 1853: } 1854: $invalid_args{$key} = 1; 1855: } 1856: } elsif($rule_name eq 'position') { 1857: if($rule_value =~ /\D/) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1858: _error($logger, "$rule_description: Parameter '$key': 'position' must be an integer"); 1859: } 1860: if($rule_value < 0) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1861: _error($logger, "$rule_description: Parameter '$key': 'position' must be a positive integer, not $value"); 1862: } 1863: } else { 1864: _error($logger, "$rule_description: Unknown rule '$rule_name'"); 1865: } 1866: } 1867: } elsif(ref($rules) eq 'ARRAY') { 1868: if(scalar(@{$rules})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1869: # An argument can be one of several different types. 1870: # This path handles both explicit array-of-rules schemas and the 1871: # normalised form of union type shorthand (type => ['a', 'b', ...]). 1872: my $rc = 0; 1873: my @types; 1874: foreach my $rule(@{$rules}) { 1875: if(ref($rule) ne 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1876: _error($logger, "$rule_description: Parameter '$key' rules must be a hash reference"); 1877: next; 1878: } 1879: if(!defined($rule->{'type'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1880: _error($logger, "$rule_description: Parameter '$key' is missing a type in an alternative"); 1881: next; 1882: } 1883: push @types, $rule->{'type'}; 1884: my $result; 1885: eval { 1886: $result = validate_strict({ input => { $key => $value }, schema => { $key => $rule }, logger => undef, custom_types => $custom_types }); 1887: }; 1888: if(!$@) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1889: # Capture coercion performed by the successful sub-validation 1890: # (e.g. integer/number coercion) so the outer scope sees it. 1891: $value = $result->{$key} if(defined($result)); 1892: $rc = 1; 1893: last; 1894: } 1895: } 1896: if(!$rc) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1897: _error($logger, "$rule_description: Parameter: '$key': must be one of " . join(', ', @types)); 1898: $invalid_args{$key} = 1; 1899: } 1900: } else { 1901: _error($logger, "$rule_description: Parameter: '$key': schema is empty arrayref"); 1902: } 1903: } elsif(ref($rules)) { 1904: _error($logger, 'rules must be a hash reference or string'); 1905: } 1906: 1907: $validated_args{$key} = $value; 1908: } 1909: 1910: # Validate parameter relationships ●1911 → 1911 → 1915 1911: if (my $relationships = $params->{'relationships'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1912: _validate_relationships(\%validated_args, $relationships, $logger, $schema_description); 1913: } 1914: ●1915 → 1915 → 1930 1915: if(my $cross_validation = $params->{'cross_validation'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1916: foreach my $validator_name(keys %{$cross_validation}) { 1917: my $validator = $cross_validation->{$validator_name}; 1918: if((!ref($validator)) || (ref($validator) ne 'CODE')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1919: _error($logger, "$schema_description: cross_validation $validator is not a code snippet"); 1920: next; 1921: } 1922: if(my $error = &{$validator}(\%validated_args, $validator)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1923: _error($logger, $error); 1924: # We have no idea which parameters are still valid, so let's invalidate them all 1925: return; 1926: } 1927: } 1928: } 1929: ●1930 → 1930 → 1934 1930: foreach my $key(keys %invalid_args) { 1931: delete $validated_args{$key}; 1932: } 1933: ●1934 → 1934 → 1951 1934: if($are_positional_args == 1) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1935: my @rc; 1936: foreach my $key (keys %{$schema}) { 1937: # Use exists() rather than if(my $value = ...) so that falsy but 1938: # valid coerced values (integer 0, empty string, undef from an 1939: # absent optional) are not silently dropped from the return array. 1940: if(exists $validated_args{$key}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1941: my $value = delete $validated_args{$key}; 1942: my $position = $schema->{$key}->{'position'}; 1943: if(defined($rc[$position])) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1944: _error($logger, "$schema_description: $key: position $position appears twice"); 1945: } 1946: $rc[$position] = $value; 1947: } 1948: } 1949: return \@rc;
Mutants (Total: 2, Killed: 2, Survived: 0)
1950: } 1951: return \%validated_args;
Mutants (Total: 2, Killed: 2, Survived: 0)
1952: } 1953: 1954: # _schema_from_arrayref($arrayref, $logger) 1955: # 1956: # Normalise an arrayref schema: 1957: # [ { name => 'param', type => 'string', ... }, ... ] 1958: # to the standard named-parameter hashref form: 1959: # { param => { type => 'string', ... }, ... } 1960: # 1961: # The 'name' key is consumed during conversion and does not become a rule. 1962: # Croaks if any element is not a hashref, is missing 'name', or if a name 1963: # appears more than once. 1964: sub _schema_from_arrayref 1965: { ●1966 → 1969 → 1980 1966: my ($arrayref, $logger) = @_; 1967: 1968: my %schema; 1969: foreach my $spec (@{$arrayref}) { 1970: _error($logger, "validate_strict: each arrayref schema element must be a hashref") 1971: unless ref($spec) eq 'HASH'; 1972: _error($logger, "validate_strict: arrayref schema element must have a 'name' key") 1973: unless exists($spec->{'name'}); 1974: my %rule = %{$spec}; 1975: my $name = delete $rule{'name'}; 1976: _error($logger, "validate_strict: duplicate parameter '$name' in arrayref schema") 1977: if exists($schema{$name}); 1978: $schema{$name} = \%rule; 1979: } 1980: return \%schema;
Mutants (Total: 2, Killed: 2, Survived: 0)
1981: } 1982: 1983: # Return number of visible characters not number of bytes 1984: # Ensure string is decoded into Perl characters 1985: sub _number_of_characters 1986: { ●1987 → 1991 → 1995 1987: my $value = $_[0]; 1988: 1989: return if(!defined($value)); 1990: 1991: if($value !~ /[^[:ascii:]]/) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1992: return length($value);
Mutants (Total: 2, Killed: 2, Survived: 0)
1993: } 1994: # Decode only if it's not already a Perl character string 1995: $value = decode_utf8($value) unless utf8::is_utf8($value); 1996: 1997: # Count grapheme clusters (visible characters) 1998: # The pseudo-operator () = forces list context to count matches 1999: # return scalar( () = $value =~ /\X/g ); 2000: 2001: return Unicode::GCString->new($value)->length();
Mutants (Total: 2, Killed: 2, Survived: 0)
2002: } 2003: 2004: sub _apply_nested_defaults { ●2005 → 2008 → 2021 2005: my ($input, $schema) = @_; 2006: my %result = %$input; 2007: 2008: foreach my $key (keys %$schema) { 2009: my $rules = $schema->{$key}; 2010: 2011: if (ref $rules eq 'HASH' && exists $rules->{default} && !exists $result{$key}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2012: $result{$key} = $rules->{default}; 2013: } 2014: 2015: # Recursively handle nested schema 2016: if((ref $rules eq 'HASH') && $rules->{schema} && (ref $result{$key} eq 'HASH')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2017: $result{$key} = _apply_nested_defaults($result{$key}, $rules->{schema}); 2018: } 2019: } 2020: 2021: return \%result;
Mutants (Total: 2, Killed: 2, Survived: 0)
2022: } 2023: 2024: sub _validate_relationships { ●2025 → 2029 → 0 2025: my ($validated_args, $relationships, $logger, $description) = @_; 2026: 2027: return unless ref($relationships) eq 'ARRAY'; 2028: 2029: foreach my $rel (@$relationships) { 2030: my $type = $rel->{type} or next; 2031: 2032: if ($type eq 'mutually_exclusive') {
Mutants (Total: 1, Killed: 1, Survived: 0)
2033: _validate_mutually_exclusive($validated_args, $rel, $logger, $description); 2034: } elsif ($type eq 'required_group') { 2035: _validate_required_group($validated_args, $rel, $logger, $description); 2036: } elsif ($type eq 'conditional_requirement') { 2037: _validate_conditional_requirement($validated_args, $rel, $logger, $description); 2038: } elsif ($type eq 'dependency') { 2039: _validate_dependency($validated_args, $rel, $logger, $description); 2040: } elsif ($type eq 'value_constraint') { 2041: _validate_value_constraint($validated_args, $rel, $logger, $description); 2042: } elsif ($type eq 'value_conditional') { 2043: _validate_value_conditional($validated_args, $rel, $logger, $description); 2044: } else { 2045: _error($logger, "Unknown relationship type $type"); 2046: } 2047: } 2048: } 2049: 2050: sub _validate_mutually_exclusive { ●2051 → 2058 → 0 2051: my ($args, $rel, $logger, $description) = @_; 2052: 2053: my @params = @{$rel->{params} || []}; 2054: return unless @params >= 2;
Mutants (Total: 3, Killed: 3, Survived: 0)
2055: 2056: my @present = grep { exists($args->{$_}) && defined($args->{$_}) } @params; 2057: 2058: if (@present > 1) {
Mutants (Total: 4, Killed: 4, Survived: 0)
2059: my $msg = $rel->{description} || 'Cannot specify both ' . join(' and ', @present); 2060: _error($logger, "$description: $msg"); 2061: } 2062: } 2063: 2064: sub _validate_required_group { ●2065 → 2072 → 0 2065: my ($args, $rel, $logger, $description) = @_; 2066: 2067: my @params = @{$rel->{params} || []}; 2068: return unless @params >= 2;
Mutants (Total: 3, Killed: 3, Survived: 0)
2069: 2070: my @present = grep { exists($args->{$_}) && defined($args->{$_}) } @params; 2071: 2072: if (@present == 0) {
Mutants (Total: 2, Killed: 2, Survived: 0)
2073: my $msg = $rel->{description} || 2074: 'Must specify at least one of: ' . join(', ', @params); 2075: _error($logger, "$description: $msg"); 2076: } 2077: } 2078: 2079: sub _validate_conditional_requirement { ●2080 → 2086 → 0 2080: my ($args, $rel, $logger, $description) = @_; 2081: 2082: my $if_param = $rel->{if} or return; 2083: my $then_param = $rel->{then_required} or return; 2084: 2085: # If the condition parameter is present and defined 2086: if (exists($args->{$if_param}) && defined($args->{$if_param})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2087: # Check if it's truthy (for booleans and general values) 2088: if ($args->{$if_param}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2089: # Then the required parameter must also be present 2090: unless (exists($args->{$then_param}) && defined($args->{$then_param})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2091: my $msg = $rel->{description} || "When $if_param is specified, $then_param is required"; 2092: _error($logger, "$description: $msg"); 2093: } 2094: } 2095: } 2096: } 2097: 2098: sub _validate_dependency { ●2099 → 2105 → 0 2099: my ($args, $rel, $logger, $description) = @_; 2100: 2101: my $param = $rel->{param} or return; 2102: my $requires = $rel->{requires} or return; 2103: 2104: # If param is present, requires must also be present 2105: if (exists($args->{$param}) && defined($args->{$param})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2106: unless (exists($args->{$requires}) && defined($args->{$requires})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2107: my $msg = $rel->{description} || "$param requires $requires to be specified"; 2108: _error($logger, "$description: $msg"); 2109: } 2110: } 2111: } 2112: 2113: sub _validate_value_constraint { ●2114 → 2123 → 0 2114: my ($args, $rel, $logger, $description) = @_; 2115: 2116: my $if_param = $rel->{if} or return; 2117: my $then_param = $rel->{then} or return; 2118: my $operator = $rel->{operator} or return; 2119: my $value = $rel->{value}; 2120: return unless defined $value; 2121: 2122: # If the condition parameter is present and truthy 2123: if (exists($args->{$if_param}) && defined($args->{$if_param}) && $args->{$if_param}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2124: # Check if the then parameter exists 2125: if (exists($args->{$then_param}) && defined($args->{$then_param})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2126: my $actual = $args->{$then_param}; 2127: my $valid = 0; 2128: 2129: if ($operator eq '==') {
Mutants (Total: 1, Killed: 1, Survived: 0)
2130: $valid = ($actual == $value);
Mutants (Total: 1, Killed: 1, Survived: 0)
2131: } elsif ($operator eq '!=') { 2132: $valid = ($actual != $value);
Mutants (Total: 1, Killed: 1, Survived: 0)
2133: } elsif ($operator eq '<') { 2134: $valid = ($actual < $value);
Mutants (Total: 3, Killed: 3, Survived: 0)
2135: } elsif ($operator eq '<=') { 2136: $valid = ($actual <= $value);
Mutants (Total: 3, Killed: 3, Survived: 0)
2137: } elsif ($operator eq '>') { 2138: $valid = ($actual > $value);
Mutants (Total: 3, Killed: 3, Survived: 0)
2139: } elsif ($operator eq '>=') { 2140: $valid = ($actual >= $value);
Mutants (Total: 3, Killed: 3, Survived: 0)
2141: } 2142: 2143: unless ($valid) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2144: my $msg = $rel->{description} || "When $if_param is specified, $then_param must be $operator $value (got $actual)"; 2145: _error($logger, "$description: $msg"); 2146: } 2147: } 2148: } 2149: } 2150: 2151: sub _validate_value_conditional { ●2152 → 2160 → 0 2152: my ($args, $rel, $logger, $description) = @_; 2153: 2154: my $if_param = $rel->{if} or return; 2155: my $equals = $rel->{equals}; 2156: my $then_param = $rel->{then_required} or return; 2157: return unless defined $equals; 2158: 2159: # If the parameter has the specific value 2160: if (exists($args->{$if_param}) && defined($args->{$if_param})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2161: if ($args->{$if_param} eq $equals) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2162: # Then the required parameter must be present 2163: unless (exists($args->{$then_param}) && defined($args->{$then_param})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2164: my $msg = $rel->{description} || 2165: "When $if_param equals '$equals', $then_param is required"; 2166: _error($logger, "$description: $msg"); 2167: } 2168: } 2169: } 2170: } 2171: 2172: # Helper to log error or croak 2173: sub _error 2174: { ●2175 → 2179 → 2182 2175: my $logger = shift; 2176: my $message = join('', @_); 2177: 2178: my @call_details = caller(0); 2179: if($logger) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2180: $logger->error(__PACKAGE__, ' line ', $call_details[2], ": $message"); 2181: } 2182: croak(__PACKAGE__, ' line ', $call_details[2], ": $message"); 2183: # Be absolutely sure, sometimes croak doesn't die for me in Test::Most scripts 2184: die (__PACKAGE__, ' line ', $call_details[2], ": $message"); 2185: } 2186: 2187: # Helper to log warning or carp 2188: sub _warn 2189: { ●2190 → 2193 → 0 2190: my $logger = shift; 2191: my $message = join('', @_); 2192: 2193: if($logger) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2194: $logger->warn(__PACKAGE__, ": $message"); 2195: } else { 2196: carp(__PACKAGE__, ": $message"); 2197: } 2198: } 2199: 2200: =head1 AUTHOR 2201: 2202: Nigel Horne, C<< <njh at nigelhorne.com> >> 2203: 2204: =encoding utf-8 2205: 2206: =head1 FORMAL SPECIFICATION 2207: 2208: [PARAM_NAME, VALUE, TYPE_NAME, CONSTRAINT_VALUE] 2209: 2210: ValidationRule ::= SimpleType | ComplexRule | UnionType 2211: 2212: SimpleType ::= string | integer | number | scalar | scalarref | stringref | arrayref | hashref | coderef | object 2213: 2214: UnionType ::= seq SimpleType -- at least two members; written as type => ['a', 'b'] 2215: 2216: ComplexRule == [ 2217: type: SimpleType | UnionType; 2218: min: ââ; 2219: max: ââ; 2220: optional: ð¹; 2221: matches: REGEX; 2222: regex: REGEX; 2223: nomatch: REGEX; 2224: memberof: seq VALUE; 2225: enum: seq VALUE; 2226: values: seq VALUE; 2227: notmemberof: seq VALUE; 2228: callback: FUNCTION; 2229: isa: TYPE_NAME; 2230: can: METHOD_NAME 2231: ] 2232: 2233: Schema == PARAM_NAME ⸠ValidationRule 2234: 2235: Arguments == PARAM_NAME ⸠VALUE 2236: 2237: ValidatedResult == PARAM_NAME ⸠VALUE 2238: 2239: â rule: ComplexRule ⢠2240: rule.min ⤠rule.max â§ 2241: ¬((rule.memberof ⨠rule.enum ⨠rule.values) â§ rule.min) â§ 2242: ¬((rule.memberof ⨠rule.enum ⨠rule.values) â§ rule.max) â§ 2243: ¬(rule.notmemberof â§ rule.min) â§ 2244: ¬(rule.notmemberof â§ rule.max) 2245: 2246: â schema: Schema; args: Arguments ⢠2247: dom(validate_strict(schema, args)) â dom(schema) ⪠dom(args) 2248: 2249: validate_strict: Schema à Arguments â ValidatedResult 2250: 2251: â schema: Schema; args: Arguments ⢠2252: let result == validate_strict(schema, args) ⢠2253: (â name: dom(schema) â© dom(args) ⢠2254: name â dom(result) â 2255: type_matches(result(name), schema(name))) â§ 2256: (â name: dom(schema) ⢠2257: ¬optional(schema(name)) â name â dom(args)) 2258: 2259: type_matches: VALUE à ValidationRule â ð¹ 2260: 2261: =head1 EXAMPLE 2262: 2263: use Params::Get; 2264: use Params::Validate::Strict; 2265: 2266: sub where_am_i 2267: { 2268: my $params = Params::Validate::Strict::validate_strict({ 2269: args => Params::Get::get_params(undef, \@_), 2270: description => 'Print a string of latitude and longitude', 2271: error_msg => 'Latitude is a number between +/- 90, longitude is a number between +/- 180', 2272: members => { 2273: 'latitude' => { 2274: type => 'number', 2275: min => -90, 2276: max => 90 2277: }, 'longitude' => { 2278: type => 'number', 2279: min => -180, 2280: max => 180 2281: } 2282: } 2283: }); 2284: 2285: print 'You are at ', $params->{'latitude'}, ', ', $params->{'longitude'}, "\n"; 2286: } 2287: 2288: where_am_i({ latitude => 3.14, longitude => -155 }); 2289: 2290: =head1 BUGS 2291: 2292: =head1 SEE ALSO 2293: 2294: =over 4 2295: 2296: =item * L<Test Dashboard|https://nigelhorne.github.io/Params-Validate-Strict/coverage/> 2297: 2298: =item * L<Data::Processor> 2299: 2300: =item * L<Params::Get> 2301: 2302: =item * L<Params::Smart> 2303: 2304: =item * L<Params::Validate> 2305: 2306: =item * L<Return::Set> 2307: 2308: =item * L<App::Test::Generator> 2309: 2310: =back 2311: 2312: =head1 SUPPORT 2313: 2314: This module is provided as-is without any warranty. 2315: 2316: Please report any bugs or feature requests to C<bug-params-validate-strict at rt.cpan.org>, 2317: or through the web interface at 2318: L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Params-Validate-Strict>. 2319: I will be notified, and then you'll 2320: automatically be notified of progress on your bug as I make changes. 2321: 2322: You can find documentation for this module with the perldoc command. 2323: 2324: perldoc Params::Validate::Strict 2325: 2326: You can also look for information at: 2327: 2328: =over 4 2329: 2330: =item * MetaCPAN 2331: 2332: L<https://metacpan.org/dist/Params-Validate-Strict> 2333: 2334: =item * RT: CPAN's request tracker 2335: 2336: L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Params-Validate-Strict> 2337: 2338: =item * CPAN Testers' Matrix 2339: 2340: L<http://matrix.cpantesters.org/?dist=Params-Validate-Strict> 2341: 2342: =item * CPAN Testers Dependencies 2343: 2344: L<http://deps.cpantesters.org/?module=Params::Validate::Strict> 2345: 2346: =back 2347: 2348: =head1 LICENSE AND COPYRIGHT 2349: 2350: Copyright 2025-2026 Nigel Horne. 2351: 2352: This program is released under the following licence: GPL2. 2353: If you use it, 2354: please let me know. 2355: 2356: =cut 2357: 2358: 1; 2359: 2360: __END__