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