TER1 (Statement): 87.89%
TER2 (Branch): 89.19%
TER3 (LCSAJ): 100.0% (27/27)
Approximate LCSAJ segments: 713
● 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: use strict; 4: use warnings; 5: 6: # TODO: test cases - check 1e20 and -1e20 are accepted as integers 7: 8: # TODOs inspired by Params::Smart (see SEE ALSO): 9: # 10: # TODO: named_only => 1 rule â marks a parameter as keyword-only; it may not 11: # be supplied by position even when the schema uses positional mode. Mirrors 12: # the '+name' sigil in Params::Smart. Useful for flags that would be 13: # dangerous or ambiguous if accidentally supplied by position (e.g. a boolean 14: # that happens to sit at the same index as a required string on a different 15: # call path). 16: # 17: # TODO: auto-detect positional vs named calling â allow a single schema to 18: # accept both f(1, 2, 3) and f(a=>1, b=>2, c=>3) by inspecting @_ at 19: # runtime and choosing the appropriate mode. Params::Smart's heuristic 20: # checks whether the first element of @_ is a known parameter name; if so, 21: # named mode is assumed, otherwise positional. Caveats: the heuristic can 22: # misfire when a positional value happens to be a string matching a param 23: # name â callers should be able to pass a hint (e.g. force_named => 1) to 24: # override. The return value should include a '_named' key (as Params::Smart 25: # does) so the caller can diagnose which mode was used. 26: # 27: # TODO: needs => ['param1', 'param2'] per-parameter dependency shorthand â 28: # a convenience alternative to the schema-level 'relationships' system. 29: # Params::Smart uses { name => 'foo', needs => ['bar'] } directly in the 30: # parameter rule. PVS already has the full dependency system via 31: # relationships => [{ type => 'dependency', ... }], but a per-parameter 32: # 'needs' key would be more ergonomic for simple one-to-many dependencies 33: # without requiring a separate top-level 'relationships' entry. Should be 34: # desugared to an equivalent dependency relationship before validation runs. 35: # 36: # TODO: '_named' diagnostic key in the return hashref â when auto-detect mode 37: # is active (see above), include a '_named' key in the returned args hashref 38: # that is true when named-parameter calling was inferred and false when 39: # positional calling was inferred. Mirrors Params::Smart's behaviour. 40: # Even without full auto-detect, this key could be set unconditionally 41: # (true for hashref input, false for arrayref input) to let callers 42: # identify which mode was actually used. 43: 44: # Remaining TODOs from Params::Util gap analysis (see SEE ALSO): 45: # 46: # TODO: element_isa => 'ClassName' rule â validates that every element of an 47: # arrayref parameter is a blessed object that passes ->isa('ClassName'). 48: # Covers Params::Util's _SET (min => 1) and _SET0 (no min) patterns, which 49: # are not expressible with the existing element_type => 'object' rule alone 50: # (that only checks blessedness, not the inheritance chain). Implementation: 51: # new 'element_isa' rule key processed inside the arrayref branch of the 52: # rule-dispatch loop, iterating each element and calling ->isa. 53: # 54: # TODO: 'can' rule extended to non-object types â three variants needed, 55: # mirroring _CLASSCAN / _INSTANCECAN / _INVOCANTCAN from Params::Util 56: # 1.105_001 (unreleased) / Params::SomeUtil (listed but not yet implemented). 57: # The motivation is to avoid the UNIVERSAL::can pitfall: always call 58: # $value->can($method) as a method (which respects an overridden can()), 59: # never UNIVERSAL::can($value, $method) as a function. PVS's existing 'can' 60: # rule for type => 'object' already calls ->can correctly. The remaining 61: # two variants to add are: 62: # - type => 'invocant' + can: value is an object OR class-name string that 63: # can do the method (_INVOCANTCAN). 64: # - type => 'string' (class-name) + classcan: string class-name that can 65: # do the method (_CLASSCAN); symmetric with classisa / classdoes. 66: 67: use Carp; 68: use Exporter qw(import); # Required for @EXPORT_OK 69: use Encode qw(decode_utf8); 70: use List::Util 1.33 qw(all any); # Required for memberof/matches validation 71: use Readonly::Values::Boolean; 72: use Scalar::Util; 73: 74: our @ISA = qw(Exporter); 75: our @EXPORT_OK = qw(validate_strict compile_schema); 76: 77: =head1 NAME 78: 79: Params::Validate::Strict - Validates a set of parameters against a schema 80: 81: =head1 VERSION 82: 83: Version 0.41 84: 85: =cut 86: 87: our $VERSION = '0.41'; 88: 89: # Recursion depth counter â localised on every entry so it unwinds automatically. 90: # Protects against mutations (or bugs) that turn the pipe-normalisation guard into 91: # an infinite loop through the union-type ARRAY handler. 92: our $_depth = 0; 93: 94: =head1 SYNOPSIS 95: 96: my $schema = { 97: username => { type => 'string', min => 3, max => 50 }, 98: age => { type => 'integer', min => 0, max => 150 }, 99: }; 100: 101: my $input = { 102: username => 'john_doe', 103: age => '30', # Will be coerced to integer 104: }; 105: 106: my $validated_input = validate_strict(schema => $schema, input => $input); 107: 108: if(defined($validated_input)) { 109: print "Example 1: Validation successful!\n"; 110: print 'Username: ', $validated_input->{username}, "\n"; 111: print 'Age: ', $validated_input->{age}, "\n"; # It's an integer now 112: } else { 113: print "Example 1: Validation failed: $@\n"; 114: } 115: 116: Upon first reading this may seem overly complex and full of scope creep in a sledgehammer to crack a nut sort of way, 117: however two use cases make use of the extensive logic that comes with this code 118: and I have a couple of other reasons for writing it. 119: 120: =over 4 121: 122: =item * Black Box Testing 123: 124: The schema can be plumbed into L<App::Test::Generator> to automatically create a set of black-box test cases. 125: 126: =item * WAF 127: 128: The schema can be plumbed into a WAF, 129: e.g., L<VWF|https://github.com/nigelhorne/VWF/>, 130: to protect from random user input. 131: 132: =item * Improved API Documentation 133: 134: Even if you don't use this module, 135: the specification syntax can help with documentation. 136: 137: =item * I like it 138: 139: I found it fun to write this, 140: even if nobody else finds it useful, 141: though I hope you will. 142: 143: =back 144: 145: =head1 METHODS 146: 147: =head2 validate_strict 148: 149: Validates a set of parameters against a schema. 150: 151: This function takes two mandatory arguments: 152: 153: =over 4 154: 155: =item * C<schema> || C<members> 156: 157: A reference to a hash that defines the validation rules for each parameter. 158: 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. 159: 160: As an alternative the schema may be supplied as an B<arrayref of parameter hashrefs>, 161: where every element describes one parameter and carries a mandatory 162: C<name> key: 163: 164: $schema = [ 165: { name => 'username', type => 'string', min => 3, max => 50 }, 166: { name => 'age', type => 'integer', min => 0, max => 150 }, 167: { name => 'role', type => 'string', optional => 1, default => 'user' }, 168: ]; 169: 170: The arrayref form is normalised to the standard hashref form before any further 171: processing. It is particularly useful when declaration order matters (e.g. 172: for positional or mixed calling conventions used by some CPAN modules). The 173: C<name> key is consumed during normalisation and does not appear as a 174: validation rule. 175: 176: For some sort of compatibility with L<Data::Processor>, 177: it is possible to wrap the schema within a hash like this: 178: 179: $schema = { 180: description => 'Describe what this schema does', 181: error_msg => 'An error message', 182: schema => { 183: # ... schema goes here 184: } 185: } 186: 187: =item * C<args> || C<input> 188: 189: A reference to a hash containing the parameters to be validated. 190: The keys of the hash are the parameter names, and the values are the parameter values. 191: 192: =back 193: 194: It takes optional arguments: 195: 196: =over 4 197: 198: =item * C<description> 199: 200: What the schema does, 201: used in error messages. 202: 203: =item * C<error_msg> 204: 205: Overrides the default message when something doesn't validate. 206: 207: =item * C<unknown_parameter_handler> 208: 209: This parameter describes what to do when a parameter is given that is not in the schema of valid parameters. 210: It must be one of C<die>, C<warn>, or C<ignore>. 211: 212: It defaults to C<die> unless C<carp_on_warn> is given, in which case it defaults to C<warn>. 213: 214: =item * C<logger> 215: 216: A logging object that understands messages such as C<error> and C<warn>. 217: 218: =item * C<custom_types> 219: 220: A reference to a hash that defines reusable custom types. 221: Custom types allow you to define validation rules once and reuse them throughout your schema, 222: making your validation logic more maintainable and readable. 223: 224: Each custom type is defined as a hash reference containing the same validation rules available for regular parameters 225: (C<type>, C<min>, C<max>, C<matches>, C<memberof>, C<values>, C<enum>, C<notmemberof>, C<callback>, etc.). 226: 227: my $custom_types = { 228: email => { 229: type => 'string', 230: matches => qr/^[^@\s]+\@[^@\s]+\.[^@\s]+$/, 231: error_msg => 'Invalid email address format' 232: }, phone => { 233: type => 'string', 234: matches => qr/^\+?[1-9]\d{1,14}$/, 235: min => 10, 236: max => 15 237: }, percentage => { 238: type => 'number', 239: min => 0, 240: max => 100 241: }, status => { 242: type => 'string', 243: memberof => ['draft', 'published', 'archived'] 244: } 245: }; 246: 247: my $schema = { 248: user_email => { type => 'email' }, 249: contact_number => { type => 'phone', optional => 1 }, 250: completion => { type => 'percentage' }, 251: post_status => { type => 'status' } 252: }; 253: 254: my $validated = validate_strict( 255: schema => $schema, 256: input => $input, 257: custom_types => $custom_types 258: ); 259: 260: Custom types can be extended or overridden in the schema by specifying additional constraints: 261: 262: my $schema = { 263: admin_username => { 264: type => 'username', # Uses custom type definition 265: min => 5, # Overrides custom type's min value 266: max => 15 # Overrides custom type's max value 267: } 268: }; 269: 270: Custom types work seamlessly with nested schema, optional parameters, and all other validation features. 271: 272: =back 273: 274: The schema can define the following rules for each parameter: 275: 276: =over 4 277: 278: =item * C<type> 279: 280: The data type of the parameter. 281: 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>, C<coderef>, C<regex>, C<handle>, C<arraylike>, C<hashlike>, C<codelike>, C<invocant> and C<void>. 282: C<scalar> accepts any plain scalar value (string, number, boolean, etc.) but rejects references (arrayrefs, hashrefs, coderefs, objects). 283: C<scalarref> accepts a reference to a scalar value (e.g. C<\$var>) but rejects plain scalars, arrayrefs, hashrefs, coderefs, and objects. 284: 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. 285: C<void> asserts that the parameter value is C<undef> (the parameter represents a void return or absent output). 286: When C<void> is used the schema must contain exactly one parameter. 287: The C<min>/C<max> constraints apply to the B<length> (in characters) of the referenced string. 288: All other string rules (C<matches>, C<nomatch>, C<memberof>, etc.) operate on the dereferenced string value. 289: The validated return value is the dereferenced plain string. 290: C<regex> accepts a compiled regular expression (C<qr//> object); the value is returned unchanged. 291: C<handle> accepts a file handle: a glob reference with a defined C<fileno>, an C<IO::Handle> subclass instance, or any value for which C<fileno> returns a defined value. 292: C<arraylike> accepts an array reference or a blessed object that overloads C<@{}> array dereferencing. 293: C<hashlike> accepts a hash reference or a blessed object that overloads C<%{}> hash dereferencing. 294: C<codelike> accepts a code reference or a blessed object that overloads C<&{}> code dereferencing. 295: C<invocant> accepts either a blessed object instance or a plain string that is a syntactically valid Perl class name (e.g. C<'MyApp::Widget'>). 296: 297: A type can be an arrayref when a parameter could have different types (e.g. a string or an object). 298: 299: $schema = { 300: username => [ 301: { type => 'string', min => 3, max => 50 }, # Name 302: { type => 'integer', 'min' => 1 }, # UID that isn't root 303: ] 304: }; 305: 306: As a shorthand, C<type> itself may be an arrayref of type name strings (a I<union type>), 307: or a pipe-separated string, when all other constraints are shared between the alternatives: 308: 309: $schema = { 310: data => { type => ['string', 'arrayref'] }, 311: id => { type => 'string|integer', optional => 1 }, 312: }; 313: 314: This is equivalent to the full array-of-rules form but more concise. 315: Whitespace around the C<|> is ignored, so C<'string | arrayref'> is the same as C<'string|arrayref'>. 316: Every other key in the rule hash (C<optional>, C<min>, C<max>, C<matches>, etc.) 317: is inherited by each candidate type and validated independently against it. 318: Type names are tried left-to-right; the first match wins and its coercion 319: (e.g. numeric types) is propagated back to the caller. 320: If the value fails all candidate types, validation croaks with a message 321: listing the union members. 322: 323: =item * C<can> 324: 325: The parameter must be an object that understands the method C<can>. 326: C<can> can be a simple scalar string of a method name, 327: or an arrayref of a list of method names, all of which must be supported by the object. 328: 329: $schema = { 330: gedcom => { type => object, can => 'get_individual' } 331: } 332: 333: =item * C<isa> 334: 335: The parameter must be an object of type C<isa>. 336: Requires C<type =E<gt> 'object'>. 337: 338: =item * C<does> 339: 340: The parameter must be a blessed object that satisfies the role via C<-E<gt>DOES>. 341: Requires C<type =E<gt> 'object'>. 342: 343: handler => { type => 'object', does => 'My::Role::Printable' } 344: 345: =item * C<classisa> 346: 347: The parameter must be a string holding a syntactically valid Perl class name 348: that passes C<-E<gt>isa('Base::Class')>. 349: The class must already be loaded (its C<@ISA> must be reachable). 350: Does not accept blessed object references; use C<isa> for those. 351: 352: backend => { type => 'string', classisa => 'My::Backend::Base' } 353: 354: =item * C<subclass> 355: 356: Like C<classisa>, but requires a I<strict> subclass: the value must not equal 357: the base class name itself. 358: 359: plugin => { type => 'string', subclass => 'My::Plugin::Base' } 360: 361: =item * C<classdoes> 362: 363: Like C<classisa>, but tests C<-E<gt>DOES> (role consumption) instead of C<-E<gt>isa>. 364: 365: consumer => { type => 'string', classdoes => 'My::Role::Loggable' } 366: 367: =item * C<driver> 368: 369: The parameter must be a valid class name that: (1) can be loaded via C<require>, 370: and (2) passes C<-E<gt>isa('Base::Class')>. 371: The module is actually loaded as a side effect of validation. 372: 373: store => { type => 'string', driver => 'Cache::Store' } 374: 375: =item * C<memberof> 376: 377: The parameter must be a member of the given arrayref. 378: 379: status => { 380: type => 'string', 381: memberof => ['draft', 'published', 'archived'] 382: } 383: 384: priority => { 385: type => 'integer', 386: memberof => [1, 2, 3, 4, 5] 387: } 388: 389: For string types, the comparison is case-sensitive by default. Use the C<case_sensitive> 390: flag to control this behavior: 391: 392: # Case-sensitive (default) - must be exact match 393: code => { 394: type => 'string', 395: memberof => ['ABC', 'DEF', 'GHI'] 396: # 'abc' will fail 397: } 398: 399: # Case-insensitive - any case accepted 400: code => { 401: type => 'string', 402: memberof => ['ABC', 'DEF', 'GHI'], 403: case_sensitive => 0 404: # 'abc', 'Abc', 'ABC' all pass, original case preserved 405: } 406: 407: For numeric types (C<integer>, C<number>, C<float>), the comparison uses numeric 408: equality (C<==> operator): 409: 410: rating => { 411: type => 'number', 412: memberof => [0.5, 1.0, 1.5, 2.0] 413: } 414: 415: Note that C<memberof> cannot be combined with C<min> or C<max> constraints as they 416: serve conflicting purposes - C<memberof> defines an explicit whitelist while C<min>/C<max> 417: define ranges. 418: 419: =item * C<enum> 420: 421: Same as C<memberof>. 422: 423: =item * C<values> 424: 425: Same as C<memberof>. 426: 427: =item * C<notmemberof> 428: 429: The parameter must not be a member of the given arrayref (blacklist). 430: This is the inverse of C<memberof>. 431: 432: username => { 433: type => 'string', 434: notmemberof => ['admin', 'root', 'system', 'administrator'] 435: } 436: 437: port => { 438: type => 'integer', 439: notmemberof => [22, 23, 25, 80, 443] # Reserved ports 440: } 441: 442: Like C<memberof>, string comparisons are case-sensitive by default but can be controlled 443: with the C<case_sensitive> flag: 444: 445: # Case-sensitive (default) 446: username => { 447: type => 'string', 448: notmemberof => ['Admin', 'Root'] 449: # 'admin' would pass, 'Admin' would fail 450: } 451: 452: # Case-insensitive 453: username => { 454: type => 'string', 455: notmemberof => ['Admin', 'Root'], 456: case_sensitive => 0 457: # 'admin', 'ADMIN', 'Admin' all fail 458: } 459: 460: The blacklist is checked after any C<transform> rules are applied, allowing you to 461: normalize input before checking: 462: 463: username => { 464: type => 'string', 465: transform => sub { lc($_[0]) }, # Normalize to lowercase 466: notmemberof => ['admin', 'root', 'system'] 467: } 468: 469: C<notmemberof> can be combined with other validation rules: 470: 471: username => { 472: type => 'string', 473: notmemberof => ['admin', 'root', 'system'], 474: min => 3, 475: max => 20, 476: matches => qr/^[a-z0-9_]+$/ 477: } 478: 479: =item * C<case_sensitive> 480: 481: A boolean value indicating whether string comparisons should be case-sensitive. 482: This flag affects the C<memberof> and C<notmemberof> validation rules. 483: The default value is C<1> (case-sensitive). 484: 485: When set to C<0>, string comparisons are performed case-insensitively, allowing values 486: with different casing to match. The original case of the input value is preserved in 487: the validated output. 488: 489: # Case-sensitive (default) 490: status => { 491: type => 'string', 492: memberof => ['Draft', 'Published', 'Archived'] # Input 'draft' will fail - must match exact case 493: } 494: 495: # Case-insensitive 496: status => { 497: type => 'string', 498: memberof => ['Draft', 'Published', 'Archived'], 499: case_sensitive => 0 # Input 'draft', 'DRAFT', or 'DrAfT' will all pass 500: } 501: 502: country_code => { 503: type => 'string', 504: memberof => ['US', 'UK', 'CA', 'FR'], 505: case_sensitive => 0 # Accept 'us', 'US', 'Us', etc. 506: } 507: 508: This flag has no effect on numeric types (C<integer>, C<number>, C<float>) as numbers 509: do not have case. 510: 511: =item * C<min>/C<minimum> 512: 513: The minimum length (for strings in characters not bytes), value (for numbers) or number of keys (for hashrefs). 514: 515: =item * C<max> 516: 517: The maximum length (for strings in characters not bytes), value (for numbers) or number of keys (for hashrefs). 518: 519: =item * C<matches> 520: 521: A regular expression that the parameter value must match. 522: Checks all members of arrayrefs. 523: 524: =item * C<nomatch> 525: 526: A regular expression that the parameter value must not match. 527: Checks all members of arrayrefs. 528: 529: =item * C<bnf> 530: 531: An arrayref of BNF grammar lines that defines the set of strings the 532: parameter value must belong to. 533: The first rule in the grammar is the start rule; the value must match it 534: exactly (anchored). 535: 536: Each element is either a rule definition (C<< <name> ::= ... >>) or a 537: continuation of the previous rule. Terminals are double-quoted; non-terminals 538: use angle brackets. Alternatives are separated by C<|>. 539: 540: $schema = { 541: na_tel_no => { 542: type => 'string', 543: bnf => [ 544: '<telephone-number> ::= <country-code-opt> <area-code> <separator-opt>', 545: '<central-office-code> <separator-opt> <station-code>', 546: '<country-code-opt> ::= "" | "+1" | "1"', 547: '<separator-opt> ::= "" | "-" | " " | "."', 548: '<area-code> ::= <digit2-9> <digit0-9> <digit0-9>', 549: '<central-office-code> ::= <digit2-9> <digit0-9> <digit0-9>', 550: '<station-code> ::= <digit0-9> <digit0-9> <digit0-9> <digit0-9>', 551: '<digit0-9> ::= "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"', 552: '<digit2-9> ::= "2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"', 553: ], 554: }, 555: }; 556: 557: Implemented by L<Params::Validate::Strict::BNF>. Recursive grammars are not 558: supported. 559: 560: =item * C<position> 561: 562: For routines and methods that take positional args, 563: this integer value defines which position the argument will be in. 564: If this is set for all arguments, 565: C<validate_strict> will return a reference to an array, rather than a reference to a hash. 566: 567: =item * C<slurp> 568: 569: Valid only in positional-argument schemas (those where every parameter has a 570: C<position> value). When C<slurp =E<gt> 1> is set, this parameter collects 571: I<all> remaining positional arguments starting from C<position> into an 572: arrayref, rather than taking only the single element at that index. 573: 574: # sub log_message($level, @messages) 575: my $schema = { 576: level => { type => 'string', position => 0 }, 577: messages => { type => 'arrayref', position => 1, slurp => 1 }, 578: }; 579: 580: The slurp parameter is implicitly optional: if there are no arguments at or 581: beyond C<position>, the value is an empty arrayref. Combine with C<min =E<gt> 582: 1> to require at least one element: 583: 584: messages => { type => 'arrayref', position => 1, slurp => 1, min => 1 } 585: 586: At most one slurp parameter may be defined per schema, and it must have the 587: highest C<position> value. The return value at that position is an arrayref. 588: 589: =item * C<aliases> 590: 591: An arrayref of alternative input-key names that are also accepted for this 592: parameter. When any alias is found in the input the parameter is stored 593: under its canonical schema key; if both the canonical name and an alias are 594: present the canonical name takes precedence. Aliases are not treated as 595: unknown parameters regardless of the C<unknown_parameter_handler> setting. 596: 597: colour => { 598: type => 'string', 599: aliases => ['color'], 600: memberof => ['red', 'green', 'blue'], 601: } 602: 603: Only named (hashref) input supports aliases; positional (arrayref) input 604: ignores them. 605: 606: =item * C<regex> 607: 608: Synonym of matches 609: 610: =item * C<description> 611: 612: The description of the rule 613: 614: =item * C<callback> 615: 616: A code reference to a subroutine that performs custom validation logic. 617: 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. 618: 619: Use this to test more complex examples: 620: 621: my $schema = { 622: even_number => { 623: type => 'integer', 624: callback => sub { $_[0] % 2 == 0 } 625: }; 626: 627: # Specify the arguments for a routine which has a second, optional argument, which, if given, must be less than or equal to the first 628: my $schema = { 629: first => { 630: type => 'integer' 631: }, second => { 632: type => 'integer', 633: optional => 1, 634: callback => sub { 635: my($value, $args) = @_; 636: # The 'defined' is needed in case 'second' is evaluated before 'first' 637: return (defined($args->{first}) && $value <= $args->{first}) ? 1 : 0 638: } 639: } 640: }; 641: 642: =item * C<optional> 643: 644: A boolean value indicating whether the parameter is optional. 645: If true, the parameter is not required. 646: If false or omitted, the parameter is required. 647: 648: It can be a reference to a code snippet that will return true or false, 649: to determine if the parameter is optional or not. 650: The code will be called with two arguments: the value of the parameter and hash ref of all parameters: 651: 652: my $schema = { 653: optional_field => { 654: type => 'string', 655: optional => sub { 656: my ($value, $all_params) = @_; 657: return $all_params->{make_optional} ? 1 : 0; 658: } 659: }, 660: make_optional => { type => 'boolean' } 661: }; 662: 663: my $result = validate_strict(schema => $schema, input => { make_optional => 1 }); 664: 665: If the parameter is not optional, it can be passed an undef value, which will not flag an error. 666: This is by design. 667: So this will not say that the required parameter 's' is missing: 668: 669: validate_strict( 670: schema => { s => { type => 'string' } }, 671: input => { s => undef }, 672: ); 673: 674: =item * C<default> 675: 676: Populate missing optional parameters with the specified value. 677: Note that this value is not validated. 678: 679: username => { 680: type => 'string', 681: optional => 1, 682: default => 'guest' 683: } 684: 685: =item * C<element_type> 686: 687: Extends the validation to individual elements of arrays. 688: 689: tags => { 690: type => 'arrayref', 691: element_type => 'number', # Float means the same 692: 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 693: max => 5 694: } 695: 696: =item * C<error_msg> 697: 698: The custom error message to be used in the event of a validation failure. 699: 700: age => { 701: type => 'integer', 702: min => 18, 703: error_msg => 'You must be at least 18 years old' 704: } 705: 706: =item * C<nullable> 707: 708: Like optional, 709: though this cannot be a coderef, 710: only a flag. 711: 712: =item * C<schema> 713: 714: You can validate nested hashrefs and arrayrefs using the C<schema> property: 715: 716: my $schema = { 717: user => { # 'user' is a hashref 718: type => 'hashref', 719: schema => { # Specify what the elements of the hash should be 720: name => { type => 'string' }, 721: age => { type => 'integer', min => 0 }, 722: hobbies => { # 'hobbies' is an array ref that this user has 723: type => 'arrayref', 724: schema => { type => 'string' }, # Validate each hobby 725: min => 1 # At least one hobby 726: } 727: } 728: }, metadata => { 729: type => 'hashref', 730: schema => { 731: created => { type => 'string' }, 732: tags => { 733: type => 'arrayref', 734: schema => { 735: type => 'string', 736: matches => qr/^[a-z]+$/ # Or you can say matches => '^[a-z]+$' 737: } 738: } 739: } 740: } 741: }; 742: 743: =item * C<validate> 744: 745: A snippet of code that validates the input. 746: It's passed the input arguments, 747: and return a string containing a reason for rejection, 748: or undef if it's allowed. 749: 750: my $schema = { 751: user => { 752: type => 'string', 753: validate => sub { 754: if($_[0]->{'password'} eq 'bar') { 755: return undef; 756: } 757: return 'Invalid password, try again'; 758: } 759: }, password => { 760: type => 'string' 761: } 762: }; 763: 764: =item * C<transform> 765: 766: A code reference to a subroutine that transforms/sanitizes the parameter value before validation. 767: The subroutine should accept the parameter value as an argument and return the transformed value. 768: The transformation is applied before any validation rules are checked, allowing you to normalize 769: or clean data before it is validated. 770: 771: Common use cases include trimming whitespace, normalizing case, formatting phone numbers, 772: sanitizing user input, and converting between data formats. 773: 774: # Simple string transformations 775: username => { 776: type => 'string', 777: transform => sub { lc(trim($_[0])) }, # lowercase and trim 778: matches => qr/^[a-z0-9_]+$/ 779: } 780: 781: email => { 782: type => 'string', 783: transform => sub { lc(trim($_[0])) }, # normalize email 784: matches => qr/^[^@\s]+\@[^@\s]+\.[^@\s]+$/ 785: } 786: 787: # Array transformations 788: tags => { 789: type => 'arrayref', 790: transform => sub { [map { lc($_) } @{$_[0]}] }, # lowercase all elements 791: element_type => 'string' 792: } 793: 794: keywords => { 795: type => 'arrayref', 796: transform => sub { 797: my @arr = map { lc(trim($_)) } @{$_[0]}; 798: my %seen; 799: return [grep { !$seen{$_}++ } @arr]; # remove duplicates 800: } 801: } 802: 803: # Numeric transformations 804: quantity => { 805: type => 'integer', 806: transform => sub { int($_[0] + 0.5) }, # round to nearest integer 807: min => 1 808: } 809: 810: # Sanitization 811: slug => { 812: type => 'string', 813: transform => sub { 814: my $str = lc(trim($_[0])); 815: $str =~ s/[^\w\s-]//g; # remove special characters 816: $str =~ s/\s+/-/g; # replace spaces with hyphens 817: return $str; 818: }, 819: matches => qr/^[a-z0-9-]+$/ 820: } 821: 822: phone => { 823: type => 'string', 824: transform => sub { 825: my $str = $_[0]; 826: $str =~ s/\D//g; # remove all non-digits 827: return $str; 828: }, 829: matches => qr/^\d{10}$/ 830: } 831: 832: The C<transform> function is applied to the value before any validation checks (C<min>/C<minimum>, C<max>, 833: C<matches>, C<callback>, etc.), ensuring that validation rules are checked against the cleaned data. 834: 835: Transformations work with all parameter types including nested structures: 836: 837: user => { 838: type => 'hashref', 839: schema => { 840: name => { 841: type => 'string', 842: transform => sub { trim($_[0]) } 843: }, email => { 844: type => 'string', 845: transform => sub { lc(trim($_[0])) } 846: } 847: } 848: } 849: 850: Transformations can also be defined in custom types for reusability: 851: 852: my $custom_types = { 853: email => { 854: type => 'string', 855: transform => sub { lc(trim($_[0])) }, 856: matches => qr/^[^@\s]+\@[^@\s]+\.[^@\s]+$/ 857: } 858: }; 859: 860: Note that the transformed value is what gets returned in the validated result and is what 861: subsequent validation rules will check against. If a transformation might fail, ensure it 862: handles edge cases appropriately. 863: It is the responsibility of the transformer to ensure that the type of the returned value is correct, 864: since that is what will be validated. 865: 866: Many validators also allow a code ref to be passed so that you can create your own, conditional validation rule, e.g.: 867: 868: $schema = { 869: age => { 870: type => 'integer', 871: min => sub { 872: my ($value, $all_params) = @_; 873: return $all_params->{country} eq 'US' ? 21 : 18; 874: } 875: } 876: } 877: 878: =item * C<validator> 879: 880: A synonym of C<validate>, for compatibility with L<Data::Processor>. 881: 882: =item * C<cross_validation> 883: 884: A reference to a hash that defines validation rules that depend on more than one parameter. 885: Cross-field validations are performed after all individual parameter validations have passed, 886: allowing you to enforce business logic that requires checking relationships between different fields. 887: 888: Each cross-validation rule is a key-value pair where the key is a descriptive name for the validation 889: and the value is a code reference that accepts a hash reference of all validated parameters. 890: The subroutine should return C<undef> if the validation passes, or an error message string if it fails. 891: 892: my $schema = { 893: password => { type => 'string', min => 8 }, 894: password_confirm => { type => 'string' } 895: }; 896: 897: my $cross_validation = { 898: passwords_match => sub { 899: my $params = shift; 900: return $params->{password} eq $params->{password_confirm} 901: ? undef : "Passwords don't match"; 902: } 903: }; 904: 905: my $validated = validate_strict( 906: schema => $schema, 907: input => $input, 908: cross_validation => $cross_validation 909: ); 910: 911: Common use cases include password confirmation, date range validation, numeric comparisons, 912: and conditional requirements: 913: 914: # Date range validation 915: my $cross_validation = { 916: date_range_valid => sub { 917: my $params = shift; 918: return $params->{start_date} le $params->{end_date} 919: ? undef : "Start date must be before or equal to end date"; 920: } 921: }; 922: 923: # Price range validation 924: my $cross_validation = { 925: price_range_valid => sub { 926: my $params = shift; 927: return $params->{min_price} <= $params->{max_price} 928: ? undef : "Minimum price must be less than or equal to maximum price"; 929: } 930: }; 931: 932: # Conditional required field 933: my $cross_validation = { 934: address_required_for_delivery => sub { 935: my $params = shift; 936: if ($params->{shipping_method} eq 'delivery' && !$params->{delivery_address}) { 937: return "Delivery address is required when shipping method is 'delivery'"; 938: } 939: return undef; 940: } 941: }; 942: 943: Multiple cross-validations can be defined in the same hash, and they are all checked in order. 944: If any cross-validation fails, the function will C<croak> with the error message returned by the validation: 945: 946: my $cross_validation = { 947: passwords_match => sub { 948: my $params = shift; 949: return $params->{password} eq $params->{password_confirm} 950: ? undef : "Passwords don't match"; 951: }, 952: emails_match => sub { 953: my $params = shift; 954: return $params->{email} eq $params->{email_confirm} 955: ? undef : "Email addresses don't match"; 956: }, 957: age_matches_birth_year => sub { 958: my $params = shift; 959: my $current_year = (localtime)[5] + 1900; 960: my $calculated_age = $current_year - $params->{birth_year}; 961: return abs($calculated_age - $params->{age}) <= 1 962: ? undef : "Age doesn't match birth year"; 963: } 964: }; 965: 966: Cross-validations receive the parameters after individual validation and transformation have been applied, 967: so you can rely on the data being in the correct format and type: 968: 969: my $schema = { 970: email => { 971: type => 'string', 972: transform => sub { lc($_[0]) } # Lowercased before cross-validation 973: }, 974: email_confirm => { 975: type => 'string', 976: transform => sub { lc($_[0]) } 977: } 978: }; 979: 980: my $cross_validation = { 981: emails_match => sub { 982: my $params = shift; 983: # Both emails are already lowercased at this point 984: return $params->{email} eq $params->{email_confirm} 985: ? undef : "Email addresses don't match"; 986: } 987: }; 988: 989: Cross-validations can access nested structures and optional fields: 990: 991: my $cross_validation = { 992: guardian_required_for_minors => sub { 993: my $params = shift; 994: if ($params->{user}{age} < 18 && !$params->{guardian}) { 995: return "Guardian information required for users under 18"; 996: } 997: return undef; 998: } 999: }; 1000: 1001: =item * metadata 1002: 1003: Fields starting with <_> are generated by L<App::Test::Generator::SchemaExtractor>, 1004: and are currently ignored. 1005: 1006: =item * C<semantic> 1007: 1008: A hint about the semantic meaning of the parameter value. 1009: Supported values: C<unix_timestamp>, C<identifier>, C<class_name>. 1010: 1011: ts => { type => 'integer', semantic => 'unix_timestamp' } 1012: func => { type => 'string', semantic => 'identifier' } 1013: module => { type => 'string', semantic => 'class_name' } 1014: 1015: When C<semantic> is C<unix_timestamp>, the value must be a non-negative integer no greater than 1016: C<2147483647> (i.e. a valid 32-bit Unix epoch timestamp). 1017: Values outside this range cause the function to C<croak>. 1018: 1019: When C<semantic> is C<identifier>, the value must match C</\A[A-Za-z_]\w*\z/>, a single 1020: valid Perl bareword identifier. Package separators (C<::>) are not permitted; use 1021: C<class_name> for those. 1022: 1023: When C<semantic> is C<class_name>, the value must match 1024: C</\A[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*\z/>, a syntactically valid Perl class name such 1025: as C<'Foo'> or C<'Foo::Bar::Baz'>. The class does not need to be loaded. 1026: 1027: Unknown semantic values emit a warning but do not cause an error. 1028: 1029: =item * schematic 1030: 1031: TODO: gives an idea of what the field will be, e.g. C<filename>. 1032: 1033: All cross-validations must pass for the overall validation to succeed. 1034: 1035: =item * C<relationships> 1036: 1037: A reference to an array that defines validation rules based on relationships between parameters. 1038: Relationship validations are performed after all individual parameter validations have passed, 1039: but before cross-validations. 1040: 1041: Each relationship is a hash reference with a C<type> field and additional fields depending on the type: 1042: 1043: =over 4 1044: 1045: =item * B<mutually_exclusive> 1046: 1047: Parameters that cannot be specified together. 1048: 1049: relationships => [ 1050: { 1051: type => 'mutually_exclusive', 1052: params => ['file', 'content'], 1053: description => 'Cannot specify both file and content' 1054: } 1055: ] 1056: 1057: =item * B<required_group> 1058: 1059: At least one parameter from the group must be specified. 1060: 1061: relationships => [ 1062: { 1063: type => 'required_group', 1064: params => ['id', 'name'], 1065: logic => 'or', 1066: description => 'Must specify either id or name' 1067: } 1068: ] 1069: 1070: =item * B<conditional_requirement> 1071: 1072: If one parameter is specified, another becomes required. 1073: 1074: relationships => [ 1075: { 1076: type => 'conditional_requirement', 1077: if => 'async', 1078: then_required => 'callback', 1079: description => 'When async is specified, callback is required' 1080: } 1081: ] 1082: 1083: =item * B<dependency> 1084: 1085: One parameter requires another to be present. 1086: 1087: relationships => [ 1088: { 1089: type => 'dependency', 1090: param => 'port', 1091: requires => 'host', 1092: description => 'port requires host to be specified' 1093: } 1094: ] 1095: 1096: =item * B<value_constraint> 1097: 1098: Specific value requirements between parameters. 1099: 1100: relationships => [ 1101: { 1102: type => 'value_constraint', 1103: if => 'ssl', 1104: then => 'port', 1105: operator => '==', 1106: value => 443, 1107: description => 'When ssl is specified, port must equal 443' 1108: } 1109: ] 1110: 1111: =item * B<value_conditional> 1112: 1113: Parameter required when another has a specific value. 1114: 1115: relationships => [ 1116: { 1117: type => 'value_conditional', 1118: if => 'mode', 1119: equals => 'secure', 1120: then_required => 'key', 1121: description => "When mode equals 'secure', key is required" 1122: } 1123: ] 1124: 1125: =back 1126: 1127: If a parameter is optional and its value is C<undef>, 1128: validation will be skipped for that parameter. 1129: 1130: If the validation fails, the function will C<croak> with an error message describing the validation failure. 1131: 1132: 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. 1133: 1134: The C<description> field is optional but recommended for clearer error messages. 1135: 1136: =back 1137: 1138: =head2 Example Usage 1139: 1140: my $schema = { 1141: host => { type => 'string' }, 1142: port => { type => 'integer' }, 1143: ssl => { type => 'boolean' }, 1144: file => { type => 'string', optional => 1 }, 1145: content => { type => 'string', optional => 1 } 1146: }; 1147: 1148: my $relationships = [ 1149: { 1150: type => 'mutually_exclusive', 1151: params => ['file', 'content'] 1152: }, { 1153: type => 'required_group', 1154: params => ['host', 'file'] 1155: }, 1156: { 1157: type => 'dependency', 1158: param => 'port', 1159: requires => 'host' 1160: }, 1161: { 1162: type => 'value_constraint', 1163: if => 'ssl', 1164: then => 'port', 1165: operator => '==', 1166: value => 443 1167: } 1168: ]; 1169: 1170: my $validated = validate_strict( 1171: schema => $schema, 1172: input => $input, 1173: relationships => $relationships 1174: ); 1175: 1176: =head1 MIGRATION FROM LEGACY VALIDATORS 1177: 1178: =head2 From L<Params::Validate> 1179: 1180: # Old style 1181: validate(@_, { 1182: name => { type => SCALAR }, 1183: age => { type => SCALAR, regex => qr/^\d+$/ } 1184: }); 1185: 1186: # New style 1187: validate_strict( 1188: schema => { # or "members" 1189: name => 'string', 1190: age => { type => 'integer', min => 0 } 1191: }, 1192: args => { @_ } 1193: ); 1194: 1195: =head2 From L<Type::Params> 1196: 1197: # Old style 1198: my ($name, $age) = validate_positional \@_, Str, Int; 1199: 1200: # New style - requires converting to named parameters first 1201: my %args = (name => $_[0], age => $_[1]); 1202: my $validated = validate_strict( 1203: schema => { name => 'string', age => 'integer' }, 1204: args => \%args 1205: ); 1206: 1207: =cut 1208: 1209: sub validate_strict 1210: { ●1211 → 1223 → 1231 1211: local $_depth = $_depth + 1; 1212: Carp::croak('validate_strict: maximum call depth exceeded â possible infinite recursion in union-type schema') 1213: if $_depth > 20;Mutants (Total: 3, Killed: 3, Survived: 0)
1214: 1215: my %args = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_; 1216: my $params = \%args; 1217: 1218: my $schema = $params->{'schema'} || $params->{'members'}; 1219: my $args = $params->{'args'} || $params->{'input'}; 1220: my $logger = $params->{'logger'}; 1221: my $custom_types = $params->{'custom_types'}; 1222: my $unknown_parameter_handler = $params->{'unknown_parameter_handler'}; 1223: if(!defined($unknown_parameter_handler)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1224: if($params->{'carp_on_warn'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1225: $unknown_parameter_handler = 'warn'; 1226: } else { 1227: $unknown_parameter_handler = 'die'; 1228: } 1229: } 1230: ●1231 → 1235 → 1240 1231: return $args if(!defined($schema)); # No schema, allow all arguments
Mutants (Total: 2, Killed: 2, Survived: 0)
1232: 1233: # Accept arrayref schema: [{ name=>'param', type=>'...', ... }, ...] 1234: # Normalise to the standard named-parameter hashref form before further processing. 1235: if(ref($schema) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1236: $schema = _schema_from_arrayref($schema, $logger); 1237: } 1238: 1239: # Check if schema and args are references to hashes ●1240 → 1240 → 1245 1240: if(ref($schema) ne 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1241: _error($logger, 'validate_strict: schema must be a hash reference'); 1242: } 1243: 1244: # Inspired by Data::Processor ●1245 → 1248 → 1258 1245: my $schema_description = $params->{'description'} || 'validate_strict'; 1246: my $error_msg = $params->{'error_msg'}; 1247: 1248: if($schema->{'members'} && ($schema->{'description'} || $schema->{'error_msg'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1249: $schema_description = $schema->{'description'}; 1250: $error_msg = $schema->{'error_msg'}; 1251: $schema = $schema->{'members'}; 1252: # The members value may also be in arrayref form 1253: if(ref($schema) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1254: $schema = _schema_from_arrayref($schema, $logger); 1255: } 1256: } 1257: ●1258 → 1258 → 1264 1258: if(exists($params->{'args'}) && (!defined($args))) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1259: $args = {}; 1260: } elsif((ref($args) ne 'HASH') && (ref($args) ne 'ARRAY')) { 1261: _error($logger, $error_msg || "$schema_description: args must be a hash or array reference"); 1262: } 1263: ●1264 → 1264 → 1295 1264: if(ref($args) eq 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1265: # Named args: build alias reverse-map first so aliased keys are not 1266: # treated as unknown parameters. 1267: my %_alias_to_canonical; 1268: foreach my $canonical (keys %{$schema}) { 1269: my $r = $schema->{$canonical}; 1270: if(ref($r) eq 'HASH' && ref($r->{'aliases'}) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1271: $_alias_to_canonical{$_} = $canonical for @{$r->{'aliases'}}; 1272: } 1273: } 1274: 1275: foreach my $key (keys %{$args}) { 1276: if(!exists($schema->{$key}) && !exists($_alias_to_canonical{$key})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1277: if($unknown_parameter_handler eq 'die') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1278: _error($logger, "$schema_description: Unknown parameter '$key'"); 1279: } elsif($unknown_parameter_handler eq 'warn') { 1280: _warn($logger, "$schema_description: Unknown parameter '$key'"); 1281: next; 1282: } elsif($unknown_parameter_handler eq 'ignore') { 1283: if($logger) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1284: $logger->debug(__PACKAGE__ . ": $schema_description: Unknown parameter '$key'"); 1285: } 1286: next; 1287: } else { 1288: _error($logger, "$schema_description: '$unknown_parameter_handler' unknown_parameter_handler must be one of die, warn, ignore"); 1289: } 1290: } 1291: } 1292: } 1293: 1294: # Find out if this routine takes positional arguments ●1295 → 1296 → 1320 1295: my $are_positional_args = -1; 1296: foreach my $key (keys %{$schema}) { 1297: if(defined(my $rules = $schema->{$key})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1298: if(ref($rules) eq 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1299: if($rules->{'slurp'} && !defined($rules->{'position'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1300: _error($logger, "::validate_strict: slurp parameter '$key' requires a 'position'"); 1301: } 1302: if(!defined($rules->{'position'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1303: if($are_positional_args == 1) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1304: _error($logger, "::validate_strict: $key is missing position value"); 1305: } 1306: $are_positional_args = 0; 1307: last; 1308: } 1309: $are_positional_args = 1; 1310: } else { 1311: $are_positional_args = 0; 1312: last; 1313: } 1314: } else { 1315: $are_positional_args = 0; 1316: last; 1317: } 1318: } 1319: ●1320 → 1322 → 2179 1320: my %validated_args; 1321: my %invalid_args; 1322: foreach my $key (keys %{$schema}) { 1323: my $rules = $schema->{$key}; 1324: 1325: # For named-arg schemas: resolve which input key provides this parameter. 1326: # If the canonical name is absent, try each alias in order. 1327: my $lookup_key = $key; 1328: if($are_positional_args != 1 && ref($rules) eq 'HASH'
Mutants (Total: 2, Killed: 2, Survived: 0)
1329: && ref($rules->{'aliases'}) eq 'ARRAY' 1330: && !exists($args->{$key})) { 1331: for my $alias (@{$rules->{'aliases'}}) { 1332: if(exists($args->{$alias})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1333: $lookup_key = $alias; 1334: last; 1335: } 1336: } 1337: } 1338: 1339: my $value; 1340: if($are_positional_args == 1) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1341: if(ref($args) ne 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1342: _error($logger, "::validate_strict: position $rules->{position} given for '$key', but args isn't an array"); 1343: } 1344: if(ref($rules) eq 'HASH' && $rules->{'slurp'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1345: my $pos = $rules->{'position'}; 1346: $value = [@{$args}[$pos .. $#$args]]; 1347: } else { 1348: $value = $args->[$rules->{'position'}]; 1349: } 1350: } else { 1351: $value = $args->{$lookup_key}; 1352: } 1353: 1354: if(!defined($rules)) { # Allow anything
Mutants (Total: 1, Killed: 1, Survived: 0)
1355: $validated_args{$key} = $value; 1356: next; 1357: } 1358: 1359: # If rules are a simple type string 1360: if(ref($rules) eq '') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1361: $rules = { type => $rules }; 1362: } 1363: 1364: my $is_optional = 0; 1365: 1366: my $rule_description = $schema_description; # Can be overridden in each element 1367: my $param_label = "'$key'"; 1368: 1369: if(ref($rules) eq 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1370: if(exists($rules->{'description'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1371: $param_label = "'$key' ($rules->{description})"; 1372: } 1373: # For stringref: validate and dereference before transform so that 1374: # transform (and all subsequent rule handlers) see the plain string. 1375: # Preserve the original ref so optional => CODE receives what the caller passed. 1376: my $pre_deref_value = $value; 1377: my $is_stringref_type = defined($value) && defined($rules->{'type'}) && !ref($rules->{'type'}) && lc($rules->{'type'}) eq 'stringref'; 1378: if($is_stringref_type) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1379: if(ref($value) ne 'SCALAR') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1380: my $got = ref($value) ? 'a ' . ref($value) . ' reference' : 'a plain scalar'; 1381: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be a string reference, not $got"); 1382: } 1383: $value = ${$value}; 1384: } 1385: if($rules->{'transform'} && defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1386: if(ref($rules->{'transform'}) eq 'CODE') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1387: $value = &{$rules->{'transform'}}($value); 1388: } else { 1389: _error($logger, "$rule_description: transforms must be a code ref"); 1390: } 1391: } 1392: if(exists($rules->{optional})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1393: if(ref($rules->{'optional'}) eq 'CODE') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1394: # For stringref the coderef receives the original SCALAR ref (what 1395: # the caller supplied), not the internally-dereferenced plain string. 1396: # For all other types the post-transform value is passed, as before. 1397: my $opt_arg = $is_stringref_type ? $pre_deref_value : $value; 1398: $is_optional = &{$rules->{optional}}($opt_arg, $args); 1399: } else { 1400: $is_optional = $rules->{'optional'}; 1401: } 1402: } elsif($rules->{nullable}) { 1403: $is_optional = $rules->{'nullable'}; 1404: } elsif(defined($rules->{'type'}) && !ref($rules->{'type'}) && lc($rules->{'type'}) eq 'void') { 1405: $is_optional = 1; 1406: } elsif($rules->{'slurp'}) { 1407: $is_optional = 1; 1408: } 1409: } 1410: 1411: # Handle optional parameters 1412: if((ref($rules) eq 'HASH') && $is_optional) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1413: my $missing; 1414: if($are_positional_args == 1) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1415: # A slurp parameter is never missing: at worst it yields an empty arrayref. 1416: $missing = $rules->{'slurp'} ? 0 : !defined($args->[$rules->{position}]); 1417: } else { 1418: $missing = !exists($args->{$lookup_key}); 1419: } 1420: if($missing) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1421: if($are_positional_args == 1) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1422: if(scalar(@{$args}) < $rules->{'position'}) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1423: # arg array is too short, so it must be missing 1424: _error($logger, "$rule_description: Required parameter $param_label is missing"); 1425: next; 1426: } 1427: } 1428: if(exists($rules->{'default'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1429: # Populate missing optional parameters with the specified output values 1430: $validated_args{$key} //= $rules->{'default'}; 1431: next; # default wins; do not fall through to the schema branch 1432: } 1433: 1434: if($rules->{'schema'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1435: $value = _apply_nested_defaults({}, $rules->{'schema'}); 1436: next unless scalar(%{$value}); 1437: # The nested schema has a default value 1438: } else { 1439: next; # optional and missing 1440: } 1441: } 1442: } elsif((ref($args) eq 'HASH') && !exists($args->{$lookup_key})) { 1443: # The parameter is required 1444: # Use exists rather than defined, so that an undefined value can be passed, but the key is there 1445: _error($logger, "$rule_description: Required parameter $param_label is missing"); 1446: } 1447: 1448: # Normalise union type shorthand: { type => ['string', 'integer'], ... } 1449: # or pipe-separated string { type => 'string|integer' } 1450: # into the array-of-rules form that the ARRAY handler below already supports. 1451: # Each candidate type inherits all other constraints from the parent rule 1452: # (min, max, matches, optional, etc.) so they are each fully validated. 1453: # Must run after optional/transform handling above but before rule dispatch below. 1454: if(ref($rules) eq 'HASH' && !ref($rules->{'type'}) && defined($rules->{'type'}) && $rules->{'type'} =~ /\|/) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1455: $rules = { %$rules, type => [ split /\s*\|\s*/, $rules->{'type'} ] }; 1456: } 1457: if(ref($rules) eq 'HASH' && ref($rules->{'type'}) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1458: my %base = %{$rules}; 1459: my @type_list = @{delete $base{'type'}}; 1460: if(!@type_list) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1461: _error($logger, "$rule_description: Parameter $param_label: union type list must not be empty"); 1462: } 1463: # Expand into one full rule hash per candidate type 1464: $rules = [ map { { %base, type => $_ } } @type_list ]; 1465: } 1466: 1467: # Validate based on rules 1468: if(ref($rules) eq 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1469: if(defined(my $min = $rules->{'min'} // $rules->{'minimum'}) && defined(my $max = $rules->{'max'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1470: if($min > $max) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1471: _error($logger, "validate_strict($key): min must be <= max ($min > $max)"); 1472: } 1473: } 1474: 1475: # memberof and its synonym enum cannot be combined with min or max 1476: if($rules->{'memberof'} || $rules->{'enum'} || $rules->{'values'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1477: if(defined(my $min = $rules->{'min'} // $rules->{'minimum'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1478: _error($logger, "validate_strict($key): min ($min) makes no sense with memberof/enum/values"); 1479: } 1480: if(defined(my $max = $rules->{'max'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1481: _error($logger, "validate_strict($key): max ($max) makes no sense with memberof/enum/values"); 1482: } 1483: } 1484: 1485: foreach my $rule_name ('type', grep { $_ ne 'type' } keys %$rules) { 1486: my $rule_value = $rules->{$rule_name}; 1487: 1488: if((ref($rule_value) eq 'CODE')
Mutants (Total: 1, Killed: 1, Survived: 0)
1489: && ($rule_name ne 'validate') 1490: && ($rule_name ne 'callback') 1491: && ($rule_name ne 'validator') 1492: && ($rule_name ne 'transform') # already applied before this loop 1493: && ($rule_name ne 'optional')) { # already applied before this loop 1494: $rule_value = &{$rule_value}($value, $args); 1495: } 1496: 1497: # Better OOP, the routine has been given an object rather than a scalar 1498: if(Scalar::Util::blessed($rule_value) && $rule_value->can('as_string')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1499: $rule_value = $rule_value->as_string(); 1500: } 1501: 1502: if($rule_name eq 'type') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1503: my $type = lc($rule_value); 1504: 1505: if(($type eq 'string') || ($type eq 'str')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1506: if(ref($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1507: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be a string"); 1508: } 1509: unless((ref($value) eq '') || (defined($value) && length($value))) { # Allow undef for optional strings
Mutants (Total: 1, Killed: 1, Survived: 0)
1510: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be a string"); 1511: } 1512: } elsif(($type eq 'integer') || ($type eq 'int')) { 1513: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1514: next; # Skip if number is undefined 1515: } 1516: if(!Scalar::Util::looks_like_number($value) || ($value - $value) != 0 || $value != int($value)) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1517: _rule_error($logger, $rules, "$rule_description: Parameter $param_label ($value) must be an integer"); 1518: } 1519: $value = int($value); # Coerce to integer 1520: } elsif(($type eq 'number') || ($type eq 'float') || ($type eq 'num') || ($type eq 'double')) { 1521: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1522: next; # Skip if number is undefined 1523: } 1524: if(!Scalar::Util::looks_like_number($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1525: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be a number"); 1526: } 1527: # $value = eval $value; # Coerce to number (be careful with eval) 1528: $value = 0 + $value; # Numeric coercion 1529: } elsif($type eq 'arrayref') { 1530: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1531: next; # Skip if arrayref is undefined 1532: } 1533: if(ref($value) ne 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1534: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be an arrayref, not " . ref($value)); 1535: } 1536: } elsif($type eq 'hashref') { 1537: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1538: next; # Skip if hashref is undefined 1539: } 1540: if(ref($value) ne 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1541: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be an hashref"); 1542: } 1543: } elsif($type eq 'scalar') { 1544: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1545: next; # Skip if undefined 1546: } 1547: if(ref($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1548: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be a scalar, not a " . ref($value) . ' reference'); 1549: } 1550: } elsif($type eq 'scalarref') { 1551: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1552: next; # Skip if undefined 1553: } 1554: if(ref($value) ne 'SCALAR') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1555: my $got = ref($value) ? 'a ' . ref($value) . ' reference' : 'a plain scalar'; 1556: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be a scalar reference, not $got"); 1557: } 1558: } elsif($type eq 'stringref') { 1559: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1560: next; # Skip if undefined 1561: } 1562: # The early-deref block validated the SCALAR ref and set $value to the 1563: # plain string. If transform subsequently returned a reference, reject it. 1564: if(ref($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1565: _rule_error($logger, $rules, "$rule_description: Parameter $param_label stringref transform must return a plain string, not a " . ref($value) . ' reference'); 1566: } 1567: } elsif($type eq 'void') { 1568: if(scalar(keys %{$schema}) != 1) {
Mutants (Total: 2, Killed: 2, Survived: 0)
1569: _error($logger, "$rule_description: type 'void' requires exactly one parameter in the schema"); 1570: } 1571: if(defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1572: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be undef (void type accepts no value)"); 1573: } 1574: } elsif(($type eq 'boolean') || ($type eq 'bool')) { 1575: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1576: next; # Skip if bool is undefined 1577: } 1578: if(defined(my $b = $Readonly::Values::Boolean::booleans{$value})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1579: $value = $b; 1580: } else { 1581: _rule_error($logger, $rules, "$rule_description: Parameter $param_label ($value) must be a boolean"); 1582: } 1583: } elsif($type eq 'coderef') { 1584: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1585: next; # Skip if code is undefined 1586: } 1587: if(ref($value) ne 'CODE') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1588: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be a coderef, not a ref to " . ref($value)); 1589: } 1590: } elsif($type eq 'regex') { 1591: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1592: next; 1593: } 1594: if(ref($value) ne 'Regexp') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1595: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be a compiled regex (qr//)" . 1596: (ref($value) ? ", not a " . ref($value) . " reference" : ", not a plain scalar")); 1597: } 1598: } elsif($type eq 'handle') { 1599: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1600: next; 1601: } 1602: my $is_handle = 0; 1603: if(ref($value) eq 'GLOB' && defined(fileno($value))) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1604: $is_handle = 1; 1605: } elsif(Scalar::Util::blessed($value) && $value->isa('IO::Handle')) { 1606: $is_handle = 1; 1607: } else { 1608: $is_handle = defined(eval { fileno($value) }); 1609: } 1610: unless($is_handle) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1611: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be a file handle"); 1612: } 1613: } elsif($type eq 'arraylike') { 1614: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1615: next; 1616: } 1617: unless(ref($value) eq 'ARRAY' || (Scalar::Util::blessed($value) && overload::Method($value, '@{}'))) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1618: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be an array reference or array-like object"); 1619: } 1620: } elsif($type eq 'hashlike') { 1621: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1622: next; 1623: } 1624: unless(ref($value) eq 'HASH' || (Scalar::Util::blessed($value) && overload::Method($value, '%{}'))) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1625: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be a hash reference or hash-like object"); 1626: } 1627: } elsif($type eq 'codelike') { 1628: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1629: next; 1630: } 1631: unless(ref($value) eq 'CODE' || (Scalar::Util::blessed($value) && overload::Method($value, '&{}'))) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1632: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be a code reference or code-like object"); 1633: } 1634: } elsif($type eq 'invocant') { 1635: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1636: next; 1637: } 1638: unless(Scalar::Util::blessed($value) || (!ref($value) && $value =~ /\A[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*\z/)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1639: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be a blessed object or a class name"); 1640: } 1641: } elsif($type eq 'object') { 1642: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1643: next; # Skip if object is undefined 1644: } 1645: if(!Scalar::Util::blessed($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1646: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must be an object"); 1647: } 1648: } elsif(my $custom_type = $custom_types->{$type}) { 1649: if($custom_type->{'transform'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1650: # The custom type has a transform embedded within it 1651: if(ref($custom_type->{'transform'}) eq 'CODE') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1652: $value = &{$custom_type->{'transform'}}($value); 1653: } else { 1654: _error($logger, "$rule_description: transforms must be a code ref"); 1655: } 1656: } 1657: validate_strict({ input => { $key => $value }, schema => { $key => $custom_type }, custom_types => $custom_types }); 1658: } else { 1659: _error($logger, "$rule_description: Unknown type '$type'"); 1660: } 1661: } elsif(($rule_name eq 'min') || ($rule_name eq 'minimum')) { 1662: if(!defined($rules->{'type'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1663: _error($logger, "$rule_description: Don't know type of $param_label to determine its minimum value $rule_value"); 1664: } 1665: my $type = lc($rules->{'type'}); 1666: if(exists($custom_types->{$type}->{'min'}) || exists($custom_types->{$type}->{minimum})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1667: $rule_value = $custom_types->{$type}->{'min'} // $custom_types->{$type}->{minimum}; 1668: $type = $custom_types->{$type}->{'type'}; 1669: } 1670: if(($type eq 'string') || ($type eq 'str') || ($type eq 'stringref')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1671: if($rule_value < 0) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1672: _rule_error($logger, $rules, "$rule_description: Parameter $param_label has meaningless minimum value that is less than zero"); 1673: } 1674: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1675: if($rule_value > 0 && !$is_optional) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1676: _rule_error($logger, $rules, "$rule_description: Parameter $param_label is undefined, but must be at least $rule_value character" . ($rule_value == 1 ? '' : 's'));
1677: $invalid_args{$key} = 1; 1678: } 1679: next; 1680: } 1681: if(defined(my $len = _number_of_characters($value))) {Mutants (Total: 1, Killed: 0, Survived: 1)
- NUM_BOUNDARY_1676_154_!=: 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)
1682: if($len < $rule_value) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1683: _rule_error($logger, $rules, "$rule_description: Parameter $param_label too short, ($len characters), must be at least $rule_value characters"); 1684: $invalid_args{$key} = 1; 1685: } 1686: } else { 1687: _rule_error($logger, $rules, "$rule_description: Parameter $param_label can't be decoded"); 1688: $invalid_args{$key} = 1; 1689: } 1690: } elsif($type eq 'arrayref') { 1691: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1692: if($rule_value > 0 && !$is_optional) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1693: _rule_error($logger, $rules, "$rule_description: Parameter $param_label is undefined, but must have at least $rule_value member" . ($rule_value > 1 ? 's' : ''));
1694: $invalid_args{$key} = 1; 1695: } 1696: next; 1697: } 1698: if(scalar(@{$value}) < $rule_value) {Mutants (Total: 3, Killed: 0, Survived: 3)
- NUM_BOUNDARY_1693_153_<: 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_1693_153_>=: 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_1693_153_<=: 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: 4, Killed: 4, Survived: 0)
1699: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must have at least $rule_value member" . (($rule_value > 1) ? 's' : ''));
1700: $invalid_args{$key} = 1; 1701: } 1702: } elsif($type eq 'hashref') { 1703: if(!defined($value)) {Mutants (Total: 3, Killed: 0, Survived: 3)
- NUM_BOUNDARY_1699_134_<: 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_1699_134_>=: 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_1699_134_<=: 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)
1704: if($rule_value > 0 && !$is_optional) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1705: _rule_error($logger, $rules, "$rule_description: Parameter $param_label is undefined, but must contain at least $rule_value keys"); 1706: $invalid_args{$key} = 1; 1707: } 1708: next; 1709: } 1710: if(scalar(keys(%{$value})) < $rule_value) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1711: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must contain at least $rule_value keys"); 1712: $invalid_args{$key} = 1; 1713: } 1714: } elsif(($type eq 'integer') || ($type eq 'number') || ($type eq 'float')) { 1715: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1716: if($rule_value > 0 && !$is_optional) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1717: _rule_error($logger, $rules, "$rule_description: Parameter $param_label is undefined, but must be at least $rule_value"); 1718: $invalid_args{$key} = 1; 1719: } 1720: next; 1721: } 1722: if(Scalar::Util::looks_like_number($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1723: if($value < $rule_value) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1724: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1725: _error($logger, $rules->{'error_msg'}); 1726: } elsif(($type eq 'integer') && ($value == 0)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1727: _error($logger, "$rule_description: Parameter $param_label ($value) must be a positive number"); 1728: } elsif(($type eq 'integer') && ($value == 1)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1729: _error($logger, "$rule_description: Parameter $param_label ($value) must be a positive, non-zero number"); 1730: } else { 1731: _error($logger, "$rule_description: Parameter $param_label ($value) must be at least $rule_value"); 1732: } 1733: $invalid_args{$key} = 1; 1734: next; 1735: } 1736: } else { 1737: _rule_error($logger, $rules, "$rule_description: Parameter $param_label ($value) must be a number"); 1738: next; 1739: } 1740: } else { 1741: _error($logger, "$rule_description: Parameter $param_label of type '$type' has meaningless min value $rule_value"); 1742: } 1743: } elsif($rule_name eq 'max') { 1744: if(!defined($rules->{'type'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1745: _error($logger, "$rule_description: Don't know type of $param_label to determine its maximum value $rule_value"); 1746: } 1747: my $type = lc($rules->{'type'}); 1748: if(exists($custom_types->{$type}->{'max'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1749: $rule_value = $custom_types->{$type}->{'max'}; 1750: $type = $custom_types->{$type}->{'type'}; 1751: } 1752: if(($type eq 'string') || ($type eq 'str') || ($type eq 'stringref')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1753: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1754: next; # Skip if string is undefined 1755: } 1756: if(defined(my $len = _number_of_characters($value))) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1757: if($len > $rule_value) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1758: _rule_error($logger, $rules, "$rule_description: Parameter $param_label too long, ($len characters), must be no longer than $rule_value"); 1759: $invalid_args{$key} = 1; 1760: } 1761: } else { 1762: _rule_error($logger, $rules, "$rule_description: Parameter $param_label can't be decoded"); 1763: $invalid_args{$key} = 1; 1764: } 1765: } elsif($type eq 'arrayref') { 1766: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1767: next; # Skip if string is undefined 1768: } 1769: if(scalar(@{$value}) > $rule_value) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1770: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must contain no more than $rule_value items"); 1771: $invalid_args{$key} = 1; 1772: } 1773: } elsif($type eq 'hashref') { 1774: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1775: next; # Skip if hash is undefined 1776: } 1777: if(scalar(keys(%{$value})) > $rule_value) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1778: _rule_error($logger, $rules, "$rule_description: Parameter $param_label must contain no more than $rule_value keys"); 1779: $invalid_args{$key} = 1; 1780: } 1781: } elsif(($type eq 'integer') || ($type eq 'number') || ($type eq 'float')) { 1782: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1783: next; # Skip if hash is undefined 1784: } 1785: if(Scalar::Util::looks_like_number($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1786: if($value > $rule_value) {
Mutants (Total: 4, Killed: 4, Survived: 0)
1787: if($rules->{'error_msg'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1788: _error($logger, $rules->{'error_msg'}); 1789: } elsif(($type eq 'integer') && ($value == 0)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1790: _error($logger, "$rule_description: Parameter $param_label ($value) must be a negative number"); 1791: } elsif(($type eq 'integer') && ($value == -1)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1792: _error($logger, "$rule_description: Parameter $param_label ($value) must be a negative, non-zero number"); 1793: } else { 1794: _error($logger, "$rule_description: Parameter $param_label ($value) must be no more than $rule_value"); 1795: } 1796: $invalid_args{$key} = 1; 1797: next; 1798: } 1799: } else { 1800: _rule_error($logger, $rules, "$rule_description: Parameter $param_label ($value) must be a number"); 1801: next; 1802: } 1803: } else { 1804: _error($logger, "$rule_description: Parameter $param_label of type '$type' has meaningless max value $rule_value"); 1805: } 1806: } elsif(($rule_name eq 'matches') || ($rule_name eq 'regex')) { 1807: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1808: next; # Skip if string is undefined 1809: } 1810: eval { 1811: my $re = (ref($rule_value) eq 'Regexp') ? $rule_value : qr/\Q$rule_value\E/; 1812: if(($rules->{'type'} eq 'arrayref') || ($rules->{'type'} eq 'ArrayRef')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1813: # all{} short-circuits on first failure and allocates no temp array 1814: unless(all { $_ =~ $re } @{$value}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1815: _rule_error($logger, $rules, "$rule_description: All members of parameter $param_label [", join(', ', @{$value}), "] must match pattern '$rule_value'"); 1816: } 1817: } elsif($value !~ $re) { 1818: _rule_error($logger, $rules, "$rule_description: Parameter $param_label ($value) must match pattern '$re'"); 1819: } 1820: 1; 1821: }; 1822: if($@) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1823: _rule_error($logger, $rules, "$rule_description: Parameter $param_label regex '$rule_value' error: $@"); 1824: $invalid_args{$key} = 1; 1825: } 1826: } elsif($rule_name eq 'nomatch') { 1827: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1828: next; # Skip if string is undefined 1829: } 1830: # Compile string patterns with \Q...\E so metacharacters are 1831: # treated as literals, matching the behaviour of 'matches'. 1832: my $re = (ref($rule_value) eq 'Regexp') ? $rule_value : qr/\Q$rule_value\E/; 1833: eval { 1834: if(($rules->{'type'} eq 'arrayref') || ($rules->{'type'} eq 'ArrayRef')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1835: # any{} short-circuits on first match and allocates no temp array 1836: if(any { $_ =~ $re } @{$value}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1837: _rule_error($logger, $rules, "$rule_description: No member of parameter $param_label [", join(', ', @{$value}), "] must match pattern '$rule_value'"); 1838: } 1839: } elsif($value =~ $re) { 1840: _rule_error($logger, $rules, "$rule_description: Parameter $param_label ($value) must not match pattern '$rule_value'"); 1841: $invalid_args{$key} = 1; 1842: } 1843: 1; 1844: }; 1845: if($@) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1846: _rule_error($logger, $rules, "$rule_description: Parameter $param_label regex '$rule_value' error: $@"); 1847: $invalid_args{$key} = 1; 1848: } 1849: } elsif($rule_name eq 'bnf') { 1850: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1851: next; # Skip if value is undefined 1852: } 1853: if(ref($rule_value) ne 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1854: _error($logger, "$rule_description: Parameter $param_label 'bnf' rule must be an arrayref of grammar lines"); 1855: } 1856: require Params::Validate::Strict::BNF; 1857: my $matcher = Params::Validate::Strict::BNF::bnf_to_matcher($rule_value); 1858: unless($matcher->($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1859: _rule_error($logger, $rules, "$rule_description: Parameter $param_label ($value) does not match the BNF grammar"); 1860: $invalid_args{$key} = 1; 1861: } 1862: } elsif(($rule_name eq 'memberof') || ($rule_name eq 'enum') || ($rule_name eq 'values')) { 1863: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1864: next; # Skip if string is undefined 1865: } 1866: if(ref($rule_value) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1867: unless(_value_in_list($value, $rule_value, $rules->{'type'} // '', $rules->{'case_sensitive'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1868: _rule_error($logger, $rules, "$rule_description: Parameter $param_label ($value) must be one of ", join(', ', @{$rule_value})); 1869: $invalid_args{$key} = 1; 1870: } 1871: } else { 1872: _rule_error($logger, $rules, "$rule_description: Parameter $param_label rule ($rule_value) must be an array reference"); 1873: } 1874: } elsif($rule_name eq 'notmemberof') { 1875: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1876: next; # Skip if string is undefined 1877: } 1878: if(ref($rule_value) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1879: if(_value_in_list($value, $rule_value, $rules->{'type'} // '', $rules->{'case_sensitive'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1880: _rule_error($logger, $rules, "$rule_description: Parameter $param_label ($value) must not be one of ", join(', ', @{$rule_value})); 1881: $invalid_args{$key} = 1; 1882: } 1883: } else { 1884: _rule_error($logger, $rules, "$rule_description: Parameter $param_label rule ($rule_value) must be an array reference"); 1885: } 1886: } elsif($rule_name eq 'isa') { 1887: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1888: next; # Skip if object not given 1889: } 1890: if($rules->{'type'} eq 'object') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1891: if(!$value->isa($rule_value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1892: _error($logger, "$rule_description: Parameter $param_label must be a '$rule_value' object got a " . (ref($value) ? ref($value) : $value) . ' object instead'); 1893: $invalid_args{$key} = 1; 1894: } 1895: } else { 1896: _error($logger, "$rule_description: Parameter $param_label has meaningless isa value $rule_value"); 1897: } 1898: } elsif($rule_name eq 'can') { 1899: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1900: next; # Skip if object not given 1901: } 1902: if($rules->{'type'} eq 'object') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1903: if(ref($rule_value) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1904: # List of methods 1905: foreach my $method(@{$rule_value}) { 1906: if(!$value->can($method)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1907: _error($logger, "$rule_description: Parameter $param_label must be an object that understands the $method method"); 1908: $invalid_args{$key} = 1; 1909: } 1910: } 1911: } elsif(!ref($rule_value)) { 1912: if(!$value->can($rule_value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1913: _error($logger, "$rule_description: Parameter $param_label must be an object that understands the $rule_value method"); 1914: $invalid_args{$key} = 1; 1915: } 1916: } else { 1917: _error($logger, "$rule_description: 'can' rule for Parameter $param_label must be either a scalar or an arrayref"); 1918: } 1919: } else { 1920: _error($logger, "$rule_description: Parameter $param_label has meaningless can value '$rule_value' for parameter type $rules->{type}"); 1921: } 1922: } elsif($rule_name eq 'does') { 1923: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1924: next; # Skip if object not given 1925: } 1926: if($rules->{'type'} eq 'object') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1927: unless(Scalar::Util::blessed($value) && $value->DOES($rule_value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1928: _error($logger, "$rule_description: Parameter $param_label must be an object that does '$rule_value'"); 1929: $invalid_args{$key} = 1; 1930: } 1931: } else { 1932: _error($logger, "$rule_description: Parameter $param_label has meaningless does value '$rule_value' for parameter type $rules->{type}"); 1933: } 1934: } elsif($rule_name eq 'classisa') { 1935: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1936: next; 1937: } 1938: unless(!ref($value) && $value =~ /\A[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*\z/ && $value->isa($rule_value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1939: _error($logger, "$rule_description: Parameter $param_label ($value) must be a class that isa '$rule_value'"); 1940: $invalid_args{$key} = 1; 1941: } 1942: } elsif($rule_name eq 'subclass') { 1943: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1944: next; 1945: } 1946: unless(!ref($value) && $value =~ /\A[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*\z/
Mutants (Total: 1, Killed: 1, Survived: 0)
1947: && $value ne $rule_value && $value->isa($rule_value)) { 1948: _error($logger, "$rule_description: Parameter $param_label ($value) must be a strict subclass of '$rule_value'"); 1949: $invalid_args{$key} = 1; 1950: } 1951: } elsif($rule_name eq 'classdoes') { 1952: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1953: next; 1954: } 1955: unless(!ref($value) && $value =~ /\A[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*\z/ && $value->DOES($rule_value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1956: _error($logger, "$rule_description: Parameter $param_label ($value) must be a class that does '$rule_value'"); 1957: $invalid_args{$key} = 1; 1958: } 1959: } elsif($rule_name eq 'driver') { 1960: if(!defined($value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1961: next; 1962: } 1963: unless(!ref($value) && $value =~ /\A[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*\z/) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1964: _error($logger, "$rule_description: Parameter $param_label must be a valid class name"); 1965: $invalid_args{$key} = 1; 1966: next; 1967: } 1968: (my $file = $value) =~ s{::}{/}g; 1969: $file .= '.pm'; 1970: eval { require $file }; 1971: if($@) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1972: _error($logger, "$rule_description: Parameter $param_label ($value) could not be loaded: $@"); 1973: $invalid_args{$key} = 1; 1974: next; 1975: } 1976: unless($value->isa($rule_value)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1977: _error($logger, "$rule_description: Parameter $param_label ($value) must be a class that isa '$rule_value'"); 1978: $invalid_args{$key} = 1; 1979: } 1980: } elsif($rule_name eq 'element_type') { 1981: if(($rules->{'type'} eq 'arrayref') || ($rules->{'type'} eq 'ArrayRef')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1982: my $type = $rule_value; 1983: my $custom_type = $custom_types->{$rule_value}; 1984: if($custom_type && $custom_type->{'type'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1985: $type = $custom_type->{'type'}; 1986: } 1987: foreach my $member(@{$value}) { 1988: if($custom_type && $custom_type->{'transform'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1989: # The custom type has a transform embedded within it 1990: if(ref($custom_type->{'transform'}) eq 'CODE') {
Mutants (Total: 1, Killed: 1, Survived: 0)
1991: $member = &{$custom_type->{'transform'}}($member); 1992: } else { 1993: _error($logger, "$rule_description: transforms must be a code ref"); 1994: } 1995: } 1996: if(($type eq 'string') || ($type eq 'Str')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1997: if(ref($member)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
1998: _rule_error($logger, $rules, "$param_label can only contain strings"); 1999: $invalid_args{$key} = 1; 2000: } 2001: } elsif($type eq 'integer') { 2002: if(ref($member) || ($member =~ /\D/)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2003: _rule_error($logger, $rules, "$param_label can only contain integers (found $member)"); 2004: $invalid_args{$key} = 1; 2005: } 2006: } elsif(($type eq 'number') || ($rule_value eq 'float')) { 2007: if(ref($member) || ($member !~ /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)$/)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2008: _rule_error($logger, $rules, "$param_label can only contain numbers (found $member)"); 2009: $invalid_args{$key} = 1; 2010: } 2011: } elsif($type eq 'object') { 2012: if(!Scalar::Util::blessed($member)) {
2013: _rule_error($logger, $rules, "$param_label can only contain objects (found $member)"); 2014: $invalid_args{$key} = 1; 2015: } 2016: } else { 2017: _error($logger, "BUG: Add $type to element_type list"); 2018: } 2019: } 2020: } else { 2021: _error($logger, "$rule_description: Parameter $param_label has meaningless element_type value $rule_value"); 2022: } 2023: } elsif($rule_name eq 'optional') { 2024: # Already handled at the beginning of the loop 2025: } elsif($rule_name eq 'nullable') { 2026: # Already handled at the beginning of the loop (same as optional) 2027: } elsif($rule_name eq 'default') { 2028: # Handled earlier 2029: } elsif($rule_name eq 'error_msg') { 2030: # Handled inline 2031: } elsif($rule_name eq 'transform') { 2032: # Handled before the loop 2033: } elsif($rule_name eq 'case_sensitive') { 2034: # Handled inline 2035: } elsif($rule_name eq 'description') { 2036: # A la, Data::Processor 2037: } elsif($rule_name =~ /^_/) { 2038: # Ignore internal/metadata fields from schema extraction 2039: } elsif($rule_name eq 'semantic') { 2040: if($rule_value eq 'unix_timestamp') {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_2012_9: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
2041: if($value < 0 || $value > 2147483647) {
Mutants (Total: 7, Killed: 7, Survived: 0)
2042: _error($logger, "Invalid Unix timestamp: $value"); 2043: } 2044: } elsif($rule_value eq 'identifier') { 2045: if(defined($value) && $value !~ /\A[A-Za-z_]\w*\z/) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2046: _error($logger, "Invalid Perl identifier: $value"); 2047: } 2048: } elsif($rule_value eq 'class_name') { 2049: if(defined($value) && $value !~ /\A[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*\z/) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2050: _error($logger, "Invalid Perl class name: $value"); 2051: } 2052: } else { 2053: _warn($logger, "semantic type $rule_value is not yet supported"); 2054: } 2055: } elsif($rule_name eq 'schema') { 2056: # Nested schema Run the given schema against each element of the array 2057: if(($rules->{'type'} eq 'arrayref') || ($rules->{'type'} eq 'ArrayRef')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2058: if(ref($value) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
2059: foreach my $member(@{$value}) { 2060: # Distinguish two schema forms: 2061: # (a) Rule hash â has a top-level 'type' key, e.g. { type=>'string', matches=>qr/.../ } 2062: # => validate each element against that rule directly. 2063: # (b) Field-schema hash â keys are field names whose values are rule hashes, 2064: # e.g. { name=>{type=>'string'}, age=>{type=>'integer'} } 2065: # => validate each hashref element against the field schema directly. 2066: my $is_field_schema = (ref($rule_value) eq 'HASH') && !exists($rule_value->{'type'}); 2067: my %inner = (custom_types => $custom_types); 2068: if($is_field_schema) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2069: $inner{input} = $member; 2070: $inner{schema} = $rule_value; 2071: } else { 2072: $inner{input} = { $key => $member }; 2073: $inner{schema} = { $key => $rule_value }; 2074: } 2075: if(!validate_strict(\%inner)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2076: $invalid_args{$key} = 1; 2077: } 2078: } 2079: } elsif(defined($value)) { # Allow undef for optional values 2080: _error($logger, "$rule_description: nested schema: Parameter '$value' must be an arrayref"); 2081: } 2082: } elsif($rules->{'type'} eq 'hashref') { 2083: if(ref($rule_value) eq 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
2084: # Apply nested defaults before validation 2085: my $nested_with_defaults = _apply_nested_defaults($value, $rule_value); 2086: if(scalar keys(%{$nested_with_defaults})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2087: if(my $new_args = validate_strict({ input => $nested_with_defaults, schema => $rule_value, custom_types => $custom_types })) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2088: $value = $new_args; 2089: } else { 2090: $invalid_args{$key} = 1; 2091: } 2092: } 2093: } else { 2094: _error($logger, "$rule_description: nested schema: Parameter '$value' must be an hashref"); 2095: } 2096: } else { 2097: _error($logger, "$rule_description: Parameter $param_label: 'schema' only supports arrayref and hashref, not $rules->{type}"); 2098: } 2099: } elsif(($rule_name eq 'validate') || ($rule_name eq 'validator')) { 2100: if(ref($rule_value) eq 'CODE') {
Mutants (Total: 1, Killed: 1, Survived: 0)
2101: if(my $error = &{$rule_value}($args)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2102: _error($logger, "$rule_description: $param_label not valid: $error"); 2103: $invalid_args{$key} = 1; 2104: } 2105: } else { 2106: # _error($logger, "$rule_description: Parameter $param_label: 'validate' only supports coderef, not $value"); 2107: _error($logger, "$rule_description: Parameter $param_label: 'validate' only supports coderef, not " . ref($rule_value) // $rule_value); 2108: } 2109: } elsif ($rule_name eq 'callback') { 2110: # Custom validation code 2111: unless (defined &$rule_value) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2112: _error($logger, "$rule_description: callback for $param_label must be a code reference"); 2113: } 2114: my $res = $rule_value->($value, $args, $schema); 2115: unless ($res) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2116: _rule_error($logger, $rules, "$rule_description: Parameter $param_label failed custom validation"); 2117: $invalid_args{$key} = 1; 2118: } 2119: } elsif($rule_name eq 'position') { 2120: if($rule_value < 0) {
Mutants (Total: 4, Killed: 4, Survived: 0)
2121: _error($logger, "$rule_description: Parameter $param_label: 'position' must be a positive integer, not $value"); 2122: } 2123: if($rule_value =~ /\D/) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2124: _error($logger, "$rule_description: Parameter $param_label: 'position' must be a positive integer"); 2125: } 2126: } elsif($rule_name eq 'slurp') { 2127: if($rule_value && $are_positional_args != 1) {
Mutants (Total: 2, Killed: 2, Survived: 0)
2128: _error($logger, "$rule_description: Parameter $param_label: 'slurp' is only valid in positional-argument schemas (all parameters need a 'position')"); 2129: } 2130: # Pre-processed: value was already collected as arrayref of remaining positional args 2131: } elsif($rule_name eq 'aliases') { 2132: # Pre-processed: alternative input-key names resolved during value fetch 2133: } else { 2134: _error($logger, "$rule_description: Unknown rule '$rule_name'"); 2135: } 2136: } 2137: } elsif(ref($rules) eq 'ARRAY') { 2138: if(scalar(@{$rules})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2139: # An argument can be one of several different types. 2140: # This path handles both explicit array-of-rules schemas and the 2141: # normalised form of union type shorthand (type => ['a', 'b', ...]). 2142: my $rc = 0; 2143: my @types; 2144: foreach my $rule(@{$rules}) { 2145: if(ref($rule) ne 'HASH') {
Mutants (Total: 1, Killed: 1, Survived: 0)
2146: _error($logger, "$rule_description: Parameter $param_label rules must be a hash reference"); 2147: } 2148: if(!defined($rule->{'type'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2149: _error($logger, "$rule_description: Parameter $param_label is missing a type in an alternative"); 2150: } 2151: push @types, $rule->{'type'}; 2152: my $result; 2153: eval { 2154: $result = validate_strict({ input => { $key => $value }, schema => { $key => $rule }, logger => undef, custom_types => $custom_types }); 2155: }; 2156: if(!$@) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2157: # Capture coercion performed by the successful sub-validation 2158: # (e.g. integer/number coercion) so the outer scope sees it. 2159: $value = $result->{$key} if(defined($result)); 2160: $rc = 1; 2161: last; 2162: } 2163: } 2164: if(!$rc) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2165: _error($logger, "$rule_description: Parameter $param_label must be one of " . join(', ', @types)); 2166: $invalid_args{$key} = 1; 2167: } 2168: } else { 2169: _error($logger, "$rule_description: Parameter $param_label schema is empty arrayref"); 2170: } 2171: } elsif(ref($rules)) { 2172: _error($logger, 'rules must be a hash reference or string'); 2173: } 2174: 2175: $validated_args{$key} = $value; 2176: } 2177: 2178: # Validate parameter relationships ●2179 → 2179 → 2183 2179: if (my $relationships = $params->{'relationships'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2180: _validate_relationships(\%validated_args, $relationships, $logger, $schema_description); 2181: } 2182: ●2183 → 2183 → 2198 2183: if(my $cross_validation = $params->{'cross_validation'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2184: foreach my $validator_name(keys %{$cross_validation}) { 2185: my $validator = $cross_validation->{$validator_name}; 2186: if((!ref($validator)) || (ref($validator) ne 'CODE')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2187: _error($logger, "$schema_description: cross_validation $validator is not a code snippet"); 2188: next; 2189: } 2190: if(my $error = &{$validator}(\%validated_args, $validator)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2191: _error($logger, $error); 2192: # We have no idea which parameters are still valid, so let's invalidate them all 2193: return; 2194: } 2195: } 2196: } 2197: ●2198 → 2198 → 2202 2198: foreach my $key(keys %invalid_args) { 2199: delete $validated_args{$key}; 2200: } 2201: ●2202 → 2202 → 2219 2202: if($are_positional_args == 1) {
Mutants (Total: 2, Killed: 2, Survived: 0)
2203: my @rc; 2204: foreach my $key (keys %{$schema}) { 2205: # Use exists() rather than if(my $value = ...) so that falsy but 2206: # valid coerced values (integer 0, empty string, undef from an 2207: # absent optional) are not silently dropped from the return array. 2208: if(exists $validated_args{$key}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2209: my $value = delete $validated_args{$key}; 2210: my $position = $schema->{$key}->{'position'}; 2211: if(defined($rc[$position])) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2212: _error($logger, "$schema_description: $key: position $position appears twice"); 2213: } 2214: $rc[$position] = $value; 2215: } 2216: } 2217: return \@rc;
Mutants (Total: 2, Killed: 2, Survived: 0)
2218: } 2219: return \%validated_args;
Mutants (Total: 2, Killed: 2, Survived: 0)
2220: } 2221: 2222: =head2 compile_schema 2223: 2224: my $validator = compile_schema(\%schema); 2225: my $result = $validator->(\%input); 2226: 2227: # with optional keyword args 2228: my $validator = compile_schema(\%schema, 2229: description => 'User registration', 2230: custom_types => \%types, 2231: unknown_parameter_handler => 'warn', 2232: ); 2233: 2234: Pre-captures a schema (and any optional keyword arguments accepted by 2235: C<validate_strict>) into a reusable validator closure. Calling the returned 2236: coderef is equivalent to: 2237: 2238: validate_strict(schema => \%schema, input => \%input, %opts); 2239: 2240: but avoids the overhead of argument parsing on every call - useful when the 2241: same schema is applied repeatedly in a hot path. 2242: 2243: =head3 Arguments 2244: 2245: =over 4 2246: 2247: =item * C<\%schema> (required) 2248: 2249: The validation schema as a hashref or arrayref, identical to the C<schema> 2250: argument of C<validate_strict>. 2251: 2252: =item * C<%opts> (optional) 2253: 2254: Any keyword arguments accepted by C<validate_strict> other than C<schema> 2255: and C<input>: C<description>, C<custom_types>, 2256: C<unknown_parameter_handler>, C<logger>, C<relationships>, 2257: C<cross_validation>, etc. 2258: 2259: =back 2260: 2261: =head3 Returns 2262: 2263: A code reference C<sub ($input) -E<gt> \%validated>. 2264: 2265: =cut 2266: 2267: sub compile_schema 2268: { ●2269 → 2270 → 2273 2269: my ($schema, %opts) = @_; 2270: unless(ref($schema) eq 'HASH' || ref($schema) eq 'ARRAY') {
Mutants (Total: 1, Killed: 1, Survived: 0)
2271: Carp::croak('compile_schema: schema must be a hash or array reference'); 2272: } 2273: return sub {
Mutants (Total: 2, Killed: 2, Survived: 0)
2274: my $input = shift; 2275: return validate_strict(schema => $schema, input => $input, %opts);
Mutants (Total: 2, Killed: 2, Survived: 0)
2276: }; 2277: } 2278: 2279: # _schema_from_arrayref($arrayref, $logger) 2280: # 2281: # Normalise an arrayref schema: 2282: # [ { name => 'param', type => 'string', ... }, ... ] 2283: # to the standard named-parameter hashref form: 2284: # { param => { type => 'string', ... }, ... } 2285: # 2286: # The 'name' key is consumed during conversion and does not become a rule. 2287: # Croaks if any element is not a hashref, is missing 'name', or if a name 2288: # appears more than once. 2289: sub _schema_from_arrayref 2290: { ●2291 → 2294 → 2305 2291: my ($arrayref, $logger) = @_; 2292: 2293: my %schema; 2294: foreach my $spec (@{$arrayref}) { 2295: _error($logger, "validate_strict: each arrayref schema element must be a hashref") 2296: unless ref($spec) eq 'HASH'; 2297: _error($logger, "validate_strict: arrayref schema element must have a 'name' key") 2298: unless exists($spec->{'name'}); 2299: my %rule = %{$spec}; 2300: my $name = delete $rule{'name'}; 2301: _error($logger, "validate_strict: duplicate parameter '$name' in arrayref schema") 2302: if exists($schema{$name}); 2303: $schema{$name} = \%rule; 2304: } 2305: return \%schema;
Mutants (Total: 2, Killed: 2, Survived: 0)
2306: } 2307: 2308: # Return number of visible characters not number of bytes 2309: # Ensure string is decoded into Perl characters 2310: sub _number_of_characters 2311: { ●2312 → 2316 → 2320 2312: my $value = $_[0]; 2313: 2314: return if(!defined($value)); 2315: 2316: if($value !~ /[^[:ascii:]]/) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2317: return length($value);
Mutants (Total: 2, Killed: 2, Survived: 0)
2318: } 2319: # Decode only if it's not already a Perl character string 2320: $value = decode_utf8($value) unless utf8::is_utf8($value); 2321: 2322: # Count grapheme clusters (visible characters). 2323: # \X matches one extended grapheme cluster; Perl's Unicode tables are kept 2324: # current with each release, correctly handling ZWJ sequences and emoji 2325: # modifier sequences that Unicode::GCString 2013.10 could not. 2326: return scalar(() = $value =~ /\X/g);
Mutants (Total: 2, Killed: 2, Survived: 0)
2327: } 2328: 2329: sub _apply_nested_defaults { ●2330 → 2333 → 2346 2330: my ($input, $schema) = @_; 2331: my %result = %$input; 2332: 2333: foreach my $key (keys %$schema) { 2334: my $rules = $schema->{$key}; 2335: 2336: if (ref $rules eq 'HASH' && exists $rules->{default} && !exists $result{$key}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2337: $result{$key} //= $rules->{default}; 2338: } 2339: 2340: # Recursively handle nested schema 2341: if((ref $rules eq 'HASH') && $rules->{schema} && (ref $result{$key} eq 'HASH')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2342: $result{$key} = _apply_nested_defaults($result{$key}, $rules->{schema}); 2343: } 2344: } 2345: 2346: return \%result;
Mutants (Total: 2, Killed: 2, Survived: 0)
2347: } 2348: 2349: sub _validate_relationships { ●2350 → 2354 → 0 2350: my ($validated_args, $relationships, $logger, $description) = @_; 2351: 2352: return unless ref($relationships) eq 'ARRAY'; 2353: 2354: foreach my $rel (@$relationships) { 2355: my $type = $rel->{type} or next; 2356: 2357: if ($type eq 'mutually_exclusive') {
Mutants (Total: 1, Killed: 1, Survived: 0)
2358: _validate_mutually_exclusive($validated_args, $rel, $logger, $description); 2359: } elsif ($type eq 'required_group') { 2360: _validate_required_group($validated_args, $rel, $logger, $description); 2361: } elsif ($type eq 'conditional_requirement') { 2362: _validate_conditional_requirement($validated_args, $rel, $logger, $description); 2363: } elsif ($type eq 'dependency') { 2364: _validate_dependency($validated_args, $rel, $logger, $description); 2365: } elsif ($type eq 'value_constraint') { 2366: _validate_value_constraint($validated_args, $rel, $logger, $description); 2367: } elsif ($type eq 'value_conditional') { 2368: _validate_value_conditional($validated_args, $rel, $logger, $description); 2369: } else { 2370: _error($logger, "Unknown relationship type $type"); 2371: } 2372: } 2373: } 2374: 2375: sub _validate_mutually_exclusive { ●2376 → 2383 → 0 2376: my ($args, $rel, $logger, $description) = @_; 2377: 2378: my @params = @{$rel->{params} || []}; 2379: return unless @params >= 2;
Mutants (Total: 3, Killed: 3, Survived: 0)
2380: 2381: my @present = grep { _param_defined($args, $_) } @params; 2382: 2383: if (@present > 1) {
Mutants (Total: 4, Killed: 4, Survived: 0)
2384: my $msg = $rel->{description} || 'Cannot specify both ' . join(' and ', @present); 2385: _error($logger, "$description: $msg"); 2386: } 2387: } 2388: 2389: sub _validate_required_group { ●2390 → 2397 → 0 2390: my ($args, $rel, $logger, $description) = @_; 2391: 2392: my @params = @{$rel->{params} || []}; 2393: return unless @params >= 2;
Mutants (Total: 3, Killed: 3, Survived: 0)
2394: 2395: my @present = grep { _param_defined($args, $_) } @params; 2396: 2397: if (@present == 0) {
Mutants (Total: 2, Killed: 2, Survived: 0)
2398: my $msg = $rel->{description} || 2399: 'Must specify at least one of: ' . join(', ', @params); 2400: _error($logger, "$description: $msg"); 2401: } 2402: } 2403: 2404: sub _validate_conditional_requirement { ●2405 → 2411 → 0 2405: my ($args, $rel, $logger, $description) = @_; 2406: 2407: my $if_param = $rel->{if} or return; 2408: my $then_param = $rel->{then_required} or return; 2409: 2410: # If the condition parameter is present and defined 2411: if (_param_defined($args, $if_param)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2412: # Check if it's truthy (for booleans and general values) 2413: if ($args->{$if_param}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2414: # Then the required parameter must also be present 2415: unless (_param_defined($args, $then_param)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2416: my $msg = $rel->{description} || "When $if_param is specified, $then_param is required"; 2417: _error($logger, "$description: $msg"); 2418: } 2419: } 2420: } 2421: } 2422: 2423: sub _validate_dependency { ●2424 → 2430 → 0 2424: my ($args, $rel, $logger, $description) = @_; 2425: 2426: my $param = $rel->{param} or return; 2427: my $requires = $rel->{requires} or return; 2428: 2429: # If param is present, requires must also be present 2430: if (_param_defined($args, $param)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2431: unless (_param_defined($args, $requires)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2432: my $msg = $rel->{description} || "$param requires $requires to be specified"; 2433: _error($logger, "$description: $msg"); 2434: } 2435: } 2436: } 2437: 2438: sub _validate_value_constraint { ●2439 → 2448 → 0 2439: my ($args, $rel, $logger, $description) = @_; 2440: 2441: my $if_param = $rel->{if} or return; 2442: my $then_param = $rel->{then} or return; 2443: my $operator = $rel->{operator} or return; 2444: my $value = $rel->{value}; 2445: return unless defined $value; 2446: 2447: # If the condition parameter is present and truthy 2448: if (_param_defined($args, $if_param) && $args->{$if_param}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2449: # Check if the then parameter exists 2450: if (_param_defined($args, $then_param)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2451: my $actual = $args->{$then_param}; 2452: my $valid = 0; 2453: 2454: if ($operator eq '==') {
Mutants (Total: 1, Killed: 1, Survived: 0)
2455: $valid = ($actual == $value);
Mutants (Total: 1, Killed: 1, Survived: 0)
2456: } elsif ($operator eq '!=') { 2457: $valid = ($actual != $value);
Mutants (Total: 1, Killed: 1, Survived: 0)
2458: } elsif ($operator eq '<') { 2459: $valid = ($actual < $value);
Mutants (Total: 3, Killed: 3, Survived: 0)
2460: } elsif ($operator eq '<=') { 2461: $valid = ($actual <= $value);
Mutants (Total: 3, Killed: 3, Survived: 0)
2462: } elsif ($operator eq '>') { 2463: $valid = ($actual > $value);
Mutants (Total: 3, Killed: 3, Survived: 0)
2464: } elsif ($operator eq '>=') { 2465: $valid = ($actual >= $value);
Mutants (Total: 3, Killed: 3, Survived: 0)
2466: } 2467: 2468: unless ($valid) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2469: my $msg = $rel->{description} || "When $if_param is specified, $then_param must be $operator $value (got $actual)"; 2470: _error($logger, "$description: $msg"); 2471: } 2472: } 2473: } 2474: } 2475: 2476: sub _validate_value_conditional { ●2477 → 2485 → 0 2477: my ($args, $rel, $logger, $description) = @_; 2478: 2479: my $if_param = $rel->{if} or return; 2480: my $equals = $rel->{equals}; 2481: my $then_param = $rel->{then_required} or return; 2482: return unless defined $equals; 2483: 2484: # If the parameter has the specific value 2485: if (_param_defined($args, $if_param)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2486: if ($args->{$if_param} eq $equals) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2487: # Then the required parameter must be present 2488: unless (_param_defined($args, $then_param)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2489: my $msg = $rel->{description} || 2490: "When $if_param equals '$equals', $then_param is required"; 2491: _error($logger, "$description: $msg"); 2492: } 2493: } 2494: } 2495: } 2496: 2497: # Emit either the rule's custom error_msg or the supplied default message. 2498: # Accepts a list for @default_parts so callers can pass join() fragments 2499: # without pre-allocating a concatenated string. 2500: sub _rule_error 2501: { 2502: my ($logger, $rules, @default_parts) = @_; 2503: _error($logger, $rules->{'error_msg'} || join('', @default_parts)); 2504: } 2505: 2506: # Package-level cache: maps "refaddr(list):mode" -> [weak_list_ref, lookup_hash]. 2507: # Each entry holds a WEAK reference to the original list arrayref alongside the 2508: # compiled lookup hash. When the list goes out of scope and is freed, the weak 2509: # reference becomes undef; the next access detects the stale entry and rebuilds, 2510: # preventing false cache hits after address reuse. 2511: my %_pvs_memberof_cache; 2512: 2513: # Return true if $value is present in $list, respecting numeric vs string 2514: # comparison and the case_sensitive flag. Used by both memberof and notmemberof. 2515: # On the first call for a given ($list, mode) pair the lookup hash is built 2516: # (O(k)); subsequent calls with the same live list object are O(1). 2517: sub _value_in_list 2518: { ●2519 → 2530 → 2535 2519: my ($value, $list, $type, $case_sensitive) = @_; 2520: my $is_numeric = ($type eq 'integer') || ($type eq 'number') || ($type eq 'float'); 2521: my $is_icase = !$is_numeric && defined($case_sensitive) && !$case_sensitive; 2522: 2523: # Key combines address and comparison mode so the same list object can be 2524: # cached under multiple modes without collision. 2525: my $ckey = Scalar::Util::refaddr($list) . ($is_numeric ? 'n' : $is_icase ? 'i' : 's'); 2526: my $entry = $_pvs_memberof_cache{$ckey}; 2527: 2528: # Stale check: if the weak ref is dead the list was freed and its address 2529: # may have been reused by a different list â discard the cached hash. 2530: if(defined($entry) && !defined($entry->[0])) {
2531: delete $_pvs_memberof_cache{$ckey}; 2532: $entry = undef; 2533: } 2534: ●2535 → 2535 → 2552 2535: unless(defined $entry) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_2530_2: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
2536: my $lookup; 2537: if($is_numeric) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2538: # Normalise to numeric value so "1" and "1.0" hash identically. 2539: $lookup = { map { ($_ + 0) => 1 } @{$list} }; 2540: } elsif($is_icase) { 2541: $lookup = { map { lc($_) => 1 } @{$list} }; 2542: } else { 2543: $lookup = { map { $_ => 1 } @{$list} }; 2544: } 2545: # Store [weak_ref_to_list, lookup_hash] â weak ref does not prevent GC. 2546: my $weak = $list; 2547: Scalar::Util::weaken($weak); 2548: $_pvs_memberof_cache{$ckey} = [$weak, $lookup]; 2549: $entry = $_pvs_memberof_cache{$ckey}; 2550: } 2551: 2552: my $lookup = $entry->[1]; 2553: return $is_numeric ? exists($lookup->{$value + 0})
Mutants (Total: 2, Killed: 2, Survived: 0)
2554: : $is_icase ? exists($lookup->{lc($value)}) 2555: : exists($lookup->{$value}); 2556: } 2557: 2558: # Return true when $args->{$param} is both present (exists) and defined. 2559: sub _param_defined 2560: { 2561: my ($args, $param) = @_; 2562: return exists($args->{$param}) && defined($args->{$param});
Mutants (Total: 2, Killed: 2, Survived: 0)
2563: } 2564: 2565: # Helper to log error or croak 2566: sub _error 2567: { ●2568 → 2575 → 2578 2568: my $logger = shift; 2569: my $message = join('', @_); 2570: # Strip ASCII control characters to prevent log-injection / CRLF attacks 2571: # when user-supplied values appear in the message. 2572: $message =~ s/[[:cntrl:]]/ /g; 2573: 2574: my @call_details = caller(0); 2575: if($logger) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2576: $logger->error(__PACKAGE__, ' line ', $call_details[2], ": $message"); 2577: } 2578: croak(__PACKAGE__, ' line ', $call_details[2], ": $message"); 2579: } 2580: 2581: # Helper to log warning or carp 2582: sub _warn 2583: { ●2584 → 2589 → 0 2584: my $logger = shift; 2585: my $message = join('', @_); 2586: # Strip ASCII control characters to prevent log-injection / CRLF attacks. 2587: $message =~ s/[[:cntrl:]]/ /g; 2588: 2589: if($logger) {
Mutants (Total: 1, Killed: 1, Survived: 0)
2590: $logger->warn(__PACKAGE__, ": $message"); 2591: } else { 2592: carp(__PACKAGE__, ": $message"); 2593: } 2594: } 2595: 2596: =head1 AUTHOR 2597: 2598: Nigel Horne, C<< <njh at nigelhorne.com> >> 2599: 2600: =encoding utf-8 2601: 2602: =head1 FORMAL SPECIFICATION 2603: 2604: [PARAM_NAME, VALUE, TYPE_NAME, CONSTRAINT_VALUE] 2605: 2606: ValidationRule ::= SimpleType | ComplexRule | UnionType 2607: 2608: SimpleType ::= string | integer | number | float | boolean | scalar 2609: | scalarref | stringref | arrayref | hashref | coderef 2610: | object | void | regex | handle 2611: | arraylike | hashlike | codelike | invocant 2612: 2613: UnionType ::= seq SimpleType -- at least two members; written as type => ['a', 'b'] 2614: 2615: ComplexRule == [ 2616: type: SimpleType | UnionType; 2617: min: ââ; 2618: max: ââ; 2619: optional: ð¹; 2620: matches: REGEX; 2621: regex: REGEX; 2622: nomatch: REGEX; 2623: memberof: seq VALUE; 2624: enum: seq VALUE; 2625: values: seq VALUE; 2626: notmemberof: seq VALUE; 2627: callback: FUNCTION; 2628: isa: TYPE_NAME; 2629: does: ROLE_NAME; 2630: can: METHOD_NAME | seq METHOD_NAME; 2631: classisa: TYPE_NAME; 2632: subclass: TYPE_NAME; 2633: classdoes: ROLE_NAME; 2634: driver: TYPE_NAME; 2635: semantic: 'unix_timestamp' | 'identifier' | 'class_name'; 2636: aliases: seq PARAM_NAME; 2637: slurp: ð¹; 2638: position: ââ; 2639: default: VALUE; 2640: transform: FUNCTION; 2641: error_msg: STRING 2642: ] 2643: 2644: Schema == PARAM_NAME ⸠ValidationRule 2645: 2646: Arguments == PARAM_NAME ⸠VALUE 2647: 2648: ValidatedResult == PARAM_NAME ⸠VALUE 2649: 2650: â rule: ComplexRule ⢠2651: rule.min ⤠rule.max â§ 2652: ¬((rule.memberof ⨠rule.enum ⨠rule.values) â§ rule.min) â§ 2653: ¬((rule.memberof ⨠rule.enum ⨠rule.values) â§ rule.max) â§ 2654: ¬(rule.notmemberof â§ rule.min) â§ 2655: ¬(rule.notmemberof â§ rule.max) 2656: 2657: â schema: Schema; args: Arguments ⢠2658: dom(validate_strict(schema, args)) â dom(schema) ⪠dom(args) 2659: 2660: validate_strict: Schema à Arguments â ValidatedResult 2661: 2662: â schema: Schema; args: Arguments ⢠2663: let result == validate_strict(schema, args) ⢠2664: (â name: dom(schema) â© dom(args) ⢠2665: name â dom(result) â 2666: type_matches(result(name), schema(name))) â§ 2667: (â name: dom(schema) ⢠2668: ¬optional(schema(name)) â name â dom(args)) 2669: 2670: type_matches: VALUE à ValidationRule â ð¹ 2671: 2672: =head1 EXAMPLE 2673: 2674: use Params::Get; 2675: use Params::Validate::Strict; 2676: 2677: sub where_am_i 2678: { 2679: my $params = Params::Validate::Strict::validate_strict({ 2680: args => Params::Get::get_params(undef, \@_), 2681: description => 'Print a string of latitude and longitude', 2682: error_msg => 'Latitude is a number between +/- 90, longitude is a number between +/- 180', 2683: members => { 2684: 'latitude' => { 2685: type => 'number', 2686: min => -90, 2687: max => 90 2688: }, 'longitude' => { 2689: type => 'number', 2690: min => -180, 2691: max => 180 2692: } 2693: } 2694: }); 2695: 2696: print 'You are at ', $params->{'latitude'}, ', ', $params->{'longitude'}, "\n"; 2697: } 2698: 2699: where_am_i({ latitude => 3.14, longitude => -155 }); 2700: 2701: =head1 BUGS 2702: 2703: =head1 SECURITY 2704: 2705: =head2 Taint mode 2706: 2707: This module does B<not> untaint its return values. 2708: When running under Perl's taint mode (C<-T>), any value that was derived from 2709: tainted external input (C<$ENV{}>, C<STDIN>, etc.) will remain tainted in the 2710: validated result, even if the module accepted it. 2711: Callers that require untainted values must perform their own regex capture after 2712: validation, for example: 2713: 2714: my $validated = validate_strict(%args); 2715: my ($safe_name) = ($validated->{name} =~ /\A([\w\s]+)\z/); 2716: 2717: =head2 User-supplied regex patterns 2718: 2719: The C<matches> rule accepts pre-compiled C<qr//> objects supplied by the caller. 2720: A pathologically constructed pattern (e.g. C<qr/(a+)+b/>) can cause catastrophic 2721: backtracking and peg a CPU core when matched against a hostile input value. 2722: Use possessive quantifiers (C<++>) or atomic groups (C<< (?>...) >>) in any 2723: C<matches> pattern that will be applied to untrusted data. 2724: 2725: =head2 Error message content 2726: 2727: Error and warning messages produced by this module may include the parameter 2728: value supplied by the caller. 2729: The module strips ASCII control characters (including CR and LF) from all 2730: messages before passing them to the logger or croaking, to prevent log-injection 2731: and HTTP response-splitting attacks. 2732: Callers should nevertheless apply their own output encoding before including any 2733: validated value in an HTTP response, HTML page, or structured log entry. 2734: 2735: =head1 SEE ALSO 2736: 2737: =over 4 2738: 2739: =item * L<Test Dashboard|https://nigelhorne.github.io/Params-Validate-Strict/coverage/> 2740: 2741: =item * L<Data::Processor> 2742: 2743: =item * L<Params::Get> 2744: 2745: =item * L<Params::Smart> 2746: 2747: This is where the ideas for C<aliases>, C<slurp> and C<compile_schema> came from. 2748: 2749: =item * L<Params::Util> 2750: 2751: This is where the ideas for C<regex>, C<handle>, C<arraylike>, C<hashlike>, C<codelike>, C<invocant> came from. 2752: 2753: =item * L<Params::SomeUtil> 2754: 2755: A maintained fork of L<Params::Util> 1.07 with bug fixes. The same type-predicate ideas apply. 2756: 2757: =item * L<Params::Validate> 2758: 2759: =item * L<Return::Set> 2760: 2761: =item * L<App::Test::Generator> 2762: 2763: =back 2764: 2765: =head1 SUPPORT 2766: 2767: This module is provided as-is without any warranty. 2768: 2769: Please report any bugs or feature requests to C<bug-params-validate-strict at rt.cpan.org>, 2770: or through the web interface at 2771: L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Params-Validate-Strict>. 2772: I will be notified, and then you'll 2773: automatically be notified of progress on your bug as I make changes. 2774: 2775: You can find documentation for this module with the perldoc command. 2776: 2777: perldoc Params::Validate::Strict 2778: 2779: You can also look for information at: 2780: 2781: =over 4 2782: 2783: =item * MetaCPAN 2784: 2785: L<https://metacpan.org/dist/Params-Validate-Strict> 2786: 2787: =item * RT: CPAN's request tracker 2788: 2789: L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Params-Validate-Strict> 2790: 2791: =item * CPAN Testers' Matrix 2792: 2793: L<http://matrix.cpantesters.org/?dist=Params-Validate-Strict> 2794: 2795: =item * CPAN Testers Dependencies 2796: 2797: L<http://deps.cpantesters.org/?module=Params::Validate::Strict> 2798: 2799: =back 2800: 2801: =head1 LICENSE AND COPYRIGHT 2802: 2803: Copyright 2025-2026 Nigel Horne. 2804: 2805: This program is released under the following licence: GPL2. 2806: If you use it, 2807: please let me know. 2808: 2809: =cut 2810: 2811: 1; 2812: 2813: __END__