lib/Params/Validate/Strict.pm

Structural Coverage (Approximate)

TER1 (Statement): 86.03%
TER2 (Branch): 85.69%
TER3 (LCSAJ): 100.0% (45/45)
Approximate LCSAJ segments: 595

LCSAJ Legend

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.

Mutant Testing Legend

Survived (tests missed this) Killed (tests detected this) No mutation
    1: package Params::Validate::Strict;
    2: 
    3: # FIXME: {max} doesn't play ball with non-ascii strings
    4: 
    5: use strict;
    6: use warnings;
    7: 
    8: use Carp;
    9: use Exporter qw(import);	# Required for @EXPORT_OK
   10: use Encode qw(decode_utf8);
   11: use List::Util 1.33 qw(any);	# Required for memberof validation
   12: use Params::Get 0.13;
   13: use Readonly::Values::Boolean;
   14: use Scalar::Util;
   15: use Unicode::GCString;
   16: 
   17: our @ISA = qw(Exporter);
   18: our @EXPORT_OK = qw(validate_strict);
   19: 
   20: =head1 NAME
   21: 
   22: Params::Validate::Strict - Validates a set of parameters against a schema
   23: 
   24: =head1 VERSION
   25: 
   26: Version 0.32
   27: 
   28: =cut
   29: 
   30: our $VERSION = '0.32';
   31: 
   32: =head1 SYNOPSIS
   33: 
   34:     my $schema = {
   35:         username => { type => 'string', min => 3, max => 50 },
   36:         age => { type => 'integer', min => 0, max => 150 },
   37:     };
   38: 
   39:     my $input = {
   40:          username => 'john_doe',
   41:          age => '30',	# Will be coerced to integer
   42:     };
   43: 
   44:     my $validated_input = validate_strict(schema => $schema, input => $input);
   45: 
   46:     if(defined($validated_input)) {
   47:         print "Example 1: Validation successful!\n";
   48:         print 'Username: ', $validated_input->{username}, "\n";
   49:         print 'Age: ', $validated_input->{age}, "\n";	# It's an integer now
   50:     } else {
   51:         print "Example 1: Validation failed: $@\n";
   52:     }
   53: 
   54: Upon first reading this may seem overly complex and full of scope creep in a sledgehammer to crack a nut sort of way,
   55: however two use cases make use of the extensive logic that comes with this code
   56: and I have a couple of other reasons for writing it.
   57: 
   58: =over 4
   59: 
   60: =item * Black Box Testing
   61: 
   62: The schema can be plumbed into L<App::Test::Generator> to automatically create a set of black-box test cases.
   63: 
   64: =item * WAF
   65: 
   66: The schema can be plumbed into a WAF to protect from random user input.
   67: 
   68: =item * Improved API Documentation
   69: 
   70: Even if you don't use this module,
   71: the specification syntax can help with documentation.
   72: 
   73: =item * I like it
   74: 
   75: I find it fun to write this,
   76: even if nobody else finds it useful,
   77: though I hope you will.
   78: 
   79: =back
   80: 
   81: =head1	METHODS
   82: 
   83: =head2 validate_strict
   84: 
   85: Validates a set of parameters against a schema.
   86: 
   87: This function takes two mandatory arguments:
   88: 
   89: =over 4
   90: 
   91: =item * C<schema> || C<members>
   92: 
   93: A reference to a hash that defines the validation rules for each parameter.
   94: The keys of the hash are the parameter names, and the values are either a string representing the parameter type or a reference to a hash containing more detailed rules.
   95: 
   96: As an alternative the schema may be supplied as an B<arrayref of parameter
   97: hashrefs>, where every element describes one parameter and carries a mandatory
   98: C<name> key:
   99: 
  100:   $schema = [
  101:     { name => 'username', type => 'string', min => 3, max => 50 },
  102:     { name => 'age',      type => 'integer', min => 0, max => 150 },
  103:     { name => 'role',     type => 'string', optional => 1, default => 'user' },
  104:   ];
  105: 
  106: The arrayref form is normalised to the standard hashref form before any further
  107: processing.  It is particularly useful when declaration order matters (e.g.
  108: for positional or mixed calling conventions used by some CPAN modules).  The
  109: C<name> key is consumed during normalisation and does not appear as a
  110: validation rule.
  111: 
  112: For some sort of compatibility with L<Data::Processor>,
  113: it is possible to wrap the schema within a hash like this:
  114: 
  115:   $schema = {
  116:     description => 'Describe what this schema does',
  117:     error_msg => 'An error message',
  118:     schema => {
  119:       # ... schema goes here
  120:     }
  121:   }
  122: 
  123: =item * C<args> || C<input>
  124: 
  125: A reference to a hash containing the parameters to be validated.
  126: The keys of the hash are the parameter names, and the values are the parameter values.
  127: 
  128: =back
  129: 
  130: It takes optional arguments:
  131: 
  132: =over 4
  133: 
  134: =item * C<description>
  135: 
  136: What the schema does,
  137: used in error messages.
  138: 
  139: =item * C<error_msg>
  140: 
  141: Overrides the default message when something doesn't validate.
  142: 
  143: =item * C<unknown_parameter_handler>
  144: 
  145: This parameter describes what to do when a parameter is given that is not in the schema of valid parameters.
  146: It must be one of C<die>, C<warn>, or C<ignore>.
  147: 
  148: It defaults to C<die> unless C<carp_on_warn> is given, in which case it defaults to C<warn>.
  149: 
  150: =item * C<logger>
  151: 
  152: A logging object that understands messages such as C<error> and C<warn>.
  153: 
  154: =item * C<custom_types>
  155: 
  156: A reference to a hash that defines reusable custom types.
  157: Custom types allow you to define validation rules once and reuse them throughout your schema,
  158: making your validation logic more maintainable and readable.
  159: 
  160: Each custom type is defined as a hash reference containing the same validation rules available for regular parameters
  161: (C<type>, C<min>, C<max>, C<matches>, C<memberof>, C<values>, C<enum>, C<notmemberof>, C<callback>, etc.).
  162: 
  163:   my $custom_types = {
  164:     email => {
  165:       type => 'string',
  166:       matches => qr/^[\w\.\-]+@[\w\.\-]+\.\w+$/,
  167:       error_msg => 'Invalid email address format'
  168:     }, phone => {
  169:       type => 'string',
  170:       matches => qr/^\+?[1-9]\d{1,14}$/,
  171:       min => 10,
  172:       max => 15
  173:     }, percentage => {
  174:       type => 'number',
  175:       min => 0,
  176:       max => 100
  177:     }, status => {
  178:       type => 'string',
  179:       memberof => ['draft', 'published', 'archived']
  180:     }
  181:   };
  182: 
  183:   my $schema = {
  184:     user_email => { type => 'email' },
  185:     contact_number => { type => 'phone', optional => 1 },
  186:     completion => { type => 'percentage' },
  187:     post_status => { type => 'status' }
  188:   };
  189: 
  190:   my $validated = validate_strict(
  191:     schema => $schema,
  192:     input => $input,
  193:     custom_types => $custom_types
  194:   );
  195: 
  196: Custom types can be extended or overridden in the schema by specifying additional constraints:
  197: 
  198:   my $schema = {
  199:     admin_username => {
  200:       type => 'username',  # Uses custom type definition
  201:       min => 5,            # Overrides custom type's min value
  202:       max => 15            # Overrides custom type's max value
  203:     }
  204:   };
  205: 
  206: Custom types work seamlessly with nested schema, optional parameters, and all other validation features.
  207: 
  208: =back
  209: 
  210: The schema can define the following rules for each parameter:
  211: 
  212: =over 4
  213: 
  214: =item * C<type>
  215: 
  216: The data type of the parameter.
  217: Valid types are C<string>, C<integer>, C<number>, C<float> C<boolean>, C<hashref>, C<arrayref>, C<object> and C<coderef>.
  218: 
  219: A type can be an arrayref when a parameter could have different types (e.g. a string or an object).
  220: 
  221:   $schema = {
  222:     username => [
  223:       { type => 'string', min => 3, max => 50 },	# Name
  224:       { type => 'integer', 'min' => 1 },	# UID that isn't root
  225:     ]
  226:   };
  227: 
  228: As a shorthand, C<type> itself may be an arrayref of type name strings (a I<union type>)
  229: when all other constraints are shared between the alternatives:
  230: 
  231:   $schema = {
  232:     data => { type => ['string', 'arrayref'] },
  233:     id   => { type => ['string', 'integer'], optional => 1 },
  234:   };
  235: 
  236: This is equivalent to the full array-of-rules form but more concise.
  237: Every other key in the rule hash (C<optional>, C<min>, C<max>, C<matches>, etc.)
  238: is inherited by each candidate type and validated independently against it.
  239: Type names are tried left-to-right; the first match wins and its coercion
  240: (e.g. numeric types) is propagated back to the caller.
  241: If the value fails all candidate types, validation croaks with a message
  242: listing the union members.
  243: 
  244: =item * C<can>
  245: 
  246: The parameter must be an object that understands the method C<can>.
  247: C<can> can be a simple scalar string of a method name,
  248: or an arrayref of a list of method names, all of which must be supported by the object.
  249: 
  250:    $schema = {
  251:      gedcom => { type => object, can => 'get_individual' }
  252:    }
  253: 
  254: =item * C<isa>
  255: 
  256: The parameter must be an object of type C<isa>.
  257: 
  258: =item * C<memberof>
  259: 
  260: The parameter must be a member of the given arrayref.
  261: 
  262:   status => {
  263:     type => 'string',
  264:     memberof => ['draft', 'published', 'archived']
  265:   }
  266: 
  267:   priority => {
  268:     type => 'integer',
  269:     memberof => [1, 2, 3, 4, 5]
  270:   }
  271: 
  272: For string types, the comparison is case-sensitive by default. Use the C<case_sensitive>
  273: flag to control this behavior:
  274: 
  275:   # Case-sensitive (default) - must be exact match
  276:   code => {
  277:     type => 'string',
  278:     memberof => ['ABC', 'DEF', 'GHI']
  279:     # 'abc' will fail
  280:   }
  281: 
  282:   # Case-insensitive - any case accepted
  283:   code => {
  284:     type => 'string',
  285:     memberof => ['ABC', 'DEF', 'GHI'],
  286:     case_sensitive => 0
  287:     # 'abc', 'Abc', 'ABC' all pass, original case preserved
  288:   }
  289: 
  290: For numeric types (C<integer>, C<number>, C<float>), the comparison uses numeric
  291: equality (C<==> operator):
  292: 
  293:   rating => {
  294:     type => 'number',
  295:     memberof => [0.5, 1.0, 1.5, 2.0]
  296:   }
  297: 
  298: Note that C<memberof> cannot be combined with C<min> or C<max> constraints as they
  299: serve conflicting purposes - C<memberof> defines an explicit whitelist while C<min>/C<max>
  300: define ranges.
  301: 
  302: =item * C<enum>
  303: 
  304: Same as C<memberof>.
  305: 
  306: =item * C<values>
  307: 
  308: Same as C<memberof>.
  309: 
  310: =item * C<notmemberof>
  311: 
  312: The parameter must not be a member of the given arrayref (blacklist).
  313: This is the inverse of C<memberof>.
  314: 
  315:   username => {
  316:     type => 'string',
  317:     notmemberof => ['admin', 'root', 'system', 'administrator']
  318:   }
  319: 
  320:   port => {
  321:     type => 'integer',
  322:     notmemberof => [22, 23, 25, 80, 443]  # Reserved ports
  323:   }
  324: 
  325: Like C<memberof>, string comparisons are case-sensitive by default but can be controlled
  326: with the C<case_sensitive> flag:
  327: 
  328:   # Case-sensitive (default)
  329:   username => {
  330:     type => 'string',
  331:     notmemberof => ['Admin', 'Root']
  332:     # 'admin' would pass, 'Admin' would fail
  333:   }
  334: 
  335:   # Case-insensitive
  336:   username => {
  337:     type => 'string',
  338:     notmemberof => ['Admin', 'Root'],
  339:     case_sensitive => 0
  340:     # 'admin', 'ADMIN', 'Admin' all fail
  341:   }
  342: 
  343: The blacklist is checked after any C<transform> rules are applied, allowing you to
  344: normalize input before checking:
  345: 
  346:   username => {
  347:     type => 'string',
  348:     transform => sub { lc($_[0]) },  # Normalize to lowercase
  349:     notmemberof => ['admin', 'root', 'system']
  350:   }
  351: 
  352: C<notmemberof> can be combined with other validation rules:
  353: 
  354:   username => {
  355:     type => 'string',
  356:     notmemberof => ['admin', 'root', 'system'],
  357:     min => 3,
  358:     max => 20,
  359:     matches => qr/^[a-z0-9_]+$/
  360:   }
  361: 
  362: =item * C<case_sensitive>
  363: 
  364: A boolean value indicating whether string comparisons should be case-sensitive.
  365: This flag affects the C<memberof> and C<notmemberof> validation rules.
  366: The default value is C<1> (case-sensitive).
  367: 
  368: When set to C<0>, string comparisons are performed case-insensitively, allowing values
  369: with different casing to match. The original case of the input value is preserved in
  370: the validated output.
  371: 
  372:   # Case-sensitive (default)
  373:   status => {
  374:     type => 'string',
  375:     memberof => ['Draft', 'Published', 'Archived'] # Input 'draft' will fail - must match exact case
  376:   }
  377: 
  378:   # Case-insensitive
  379:   status => {
  380:     type => 'string',
  381:     memberof => ['Draft', 'Published', 'Archived'],
  382:     case_sensitive => 0 # Input 'draft', 'DRAFT', or 'DrAfT' will all pass
  383:   }
  384: 
  385:   country_code => {
  386:     type => 'string',
  387:     memberof => ['US', 'UK', 'CA', 'FR'],
  388:     case_sensitive => 0  # Accept 'us', 'US', 'Us', etc.
  389:   }
  390: 
  391: This flag has no effect on numeric types (C<integer>, C<number>, C<float>) as numbers
  392: do not have case.
  393: 
  394: =item * C<min>/C<minimum>
  395: 
  396: The minimum length (for strings in characters not bytes), value (for numbers) or number of keys (for hashrefs).
  397: 
  398: =item * C<max>
  399: 
  400: The maximum length (for strings in characters not bytes), value (for numbers) or number of keys (for hashrefs).
  401: 
  402: =item * C<matches>
  403: 
  404: A regular expression that the parameter value must match.
  405: Checks all members of arrayrefs.
  406: 
  407: =item * C<nomatch>
  408: 
  409: A regular expression that the parameter value must not match.
  410: Checks all members of arrayrefs.
  411: 
  412: =item * C<position>
  413: 
  414: For routines and methods that take positional args,
  415: this integer value defines which position the argument will be in.
  416: If this is set for all arguments,
  417: C<validate_strict> will return a reference to an array, rather than a reference to a hash.
  418: 
  419: =item * C<regex>
  420: 
  421: Synonym of matches
  422: 
  423: =item * C<description>
  424: 
  425: The description of the rule
  426: 
  427: =item * C<callback>
  428: 
  429: A code reference to a subroutine that performs custom validation logic.
  430: The subroutine should accept the parameter value, the argument list and the schema as arguments and return true if the value is valid, false otherwise.
  431: 
  432: Use this to test more complex examples:
  433: 
  434:   my $schema = {
  435:     even_number => {
  436:       type => 'integer',
  437:       callback => sub { $_[0] % 2 == 0 }
  438:   };
  439: 
  440:   # Specify the arguments for a routine which has a second, optional argument, which, if given, must be less than or equal to the first
  441:   my $schema = {
  442:     first => {
  443:       type => 'integer'
  444:     }, second => {
  445:       type => 'integer',
  446:       optional => 1,
  447:       callback => sub {
  448:         my($value, $args) = @_;
  449: 	# The 'defined' is needed in case 'second' is evaluated before 'first'
  450: 	return (defined($args->{first}) && $value <= $args->{first}) ? 1 : 0
  451:       }
  452:     }
  453:   };
  454: 
  455: =item * C<optional>
  456: 
  457: A boolean value indicating whether the parameter is optional.
  458: If true, the parameter is not required.
  459: If false or omitted, the parameter is required.
  460: 
  461: It can be a reference to a code snippet that will return true or false,
  462: to determine if the parameter is optional or not.
  463: The code will be called with two arguments: the value of the parameter and hash ref of all parameters:
  464: 
  465:   my $schema = {
  466:     optional_field => {
  467:       type => 'string',
  468:       optional => sub {
  469:         my ($value, $all_params) = @_;
  470:         return $all_params->{make_optional} ? 1 : 0;
  471:       }
  472:     },
  473:     make_optional => { type => 'boolean' }
  474:   };
  475: 
  476:   my $result = validate_strict(schema => $schema, input => { make_optional => 1 });
  477: 
  478: If the parameter is not optional, it can be passed an undef value, which will not flag an error.
  479: This is by design.
  480: So this will not say that the required parameter 's' is missing:
  481: 
  482:     validate_strict(
  483:         schema => { s => { type => 'string' } },
  484:         input  => { s => undef },
  485:     );
  486: 
  487: =item * C<default>
  488: 
  489: Populate missing optional parameters with the specified value.
  490: Note that this value is not validated.
  491: 
  492:   username => {
  493:     type => 'string',
  494:     optional => 1,
  495:     default => 'guest'
  496:   }
  497: 
  498: =item * C<element_type>
  499: 
  500: Extends the validation to individual elements of arrays.
  501: 
  502:   tags => {
  503:     type => 'arrayref',
  504:     element_type => 'number',	# Float means the same
  505:     min => 1,	# this is the length of the array, not the min value for each of the numbers. For that, add a C<schema> rule
  506:     max => 5
  507:   }
  508: 
  509: =item * C<error_msg>
  510: 
  511: The custom error message to be used in the event of a validation failure.
  512: 
  513:   age => {
  514:     type => 'integer',
  515:     min => 18,
  516:     error_msg => 'You must be at least 18 years old'
  517:   }
  518: 
  519: =item * C<nullable>
  520: 
  521: Like optional,
  522: though this cannot be a coderef,
  523: only a flag.
  524: 
  525: =item * C<schema>
  526: 
  527: You can validate nested hashrefs and arrayrefs using the C<schema> property:
  528: 
  529:     my $schema = {
  530:         user => {	# 'user' is a hashref
  531:             type => 'hashref',
  532:             schema => {	# Specify what the elements of the hash should be
  533:                 name => { type => 'string' },
  534:                 age => { type => 'integer', min => 0 },
  535:                 hobbies => {	# 'hobbies' is an array ref that this user has
  536:                     type => 'arrayref',
  537:                     schema => { type => 'string' }, # Validate each hobby
  538:                     min => 1 # At least one hobby
  539:                 }
  540:             }
  541:         }, metadata => {
  542:             type => 'hashref',
  543:             schema => {
  544:                 created => { type => 'string' },
  545:                 tags => {
  546:                     type => 'arrayref',
  547:                     schema => {
  548:                         type => 'string',
  549:                         matches => qr/^[a-z]+$/	# Or you can say matches => '^[a-z]+$'
  550:                     }
  551:                 }
  552:             }
  553:         }
  554:     };
  555: 
  556: =item * C<validate>
  557: 
  558: A snippet of code that validates the input.
  559: It's passed the input arguments,
  560: and return a string containing a reason for rejection,
  561: or undef if it's allowed.
  562: 
  563:     my $schema = {
  564:       user => {
  565:         type => 'string',
  566: 	validate => sub {
  567: 	  if($_[0]->{'password'} eq 'bar') {
  568: 	    return undef;
  569: 	  }
  570: 	  return 'Invalid password, try again';
  571: 	}
  572:       }, password => {
  573:          type => 'string'
  574:       }
  575:     };
  576: 
  577: =item * C<transform>
  578: 
  579: A code reference to a subroutine that transforms/sanitizes the parameter value before validation.
  580: The subroutine should accept the parameter value as an argument and return the transformed value.
  581: The transformation is applied before any validation rules are checked, allowing you to normalize
  582: or clean data before it is validated.
  583: 
  584: Common use cases include trimming whitespace, normalizing case, formatting phone numbers,
  585: sanitizing user input, and converting between data formats.
  586: 
  587:   # Simple string transformations
  588:   username => {
  589:     type => 'string',
  590:     transform => sub { lc(trim($_[0])) },  # lowercase and trim
  591:     matches => qr/^[a-z0-9_]+$/
  592:   }
  593: 
  594:   email => {
  595:     type => 'string',
  596:     transform => sub { lc(trim($_[0])) },  # normalize email
  597:     matches => qr/^[\w\.\-]+@[\w\.\-]+\.\w+$/
  598:   }
  599: 
  600:   # Array transformations
  601:   tags => {
  602:     type => 'arrayref',
  603:     transform => sub { [map { lc($_) } @{$_[0]}] },  # lowercase all elements
  604:     element_type => 'string'
  605:   }
  606: 
  607:   keywords => {
  608:     type => 'arrayref',
  609:     transform => sub {
  610:       my @arr = map { lc(trim($_)) } @{$_[0]};
  611:       my %seen;
  612:       return [grep { !$seen{$_}++ } @arr];  # remove duplicates
  613:     }
  614:   }
  615: 
  616:   # Numeric transformations
  617:   quantity => {
  618:     type => 'integer',
  619:     transform => sub { int($_[0] + 0.5) },  # round to nearest integer
  620:     min => 1
  621:   }
  622: 
  623:   # Sanitization
  624:   slug => {
  625:     type => 'string',
  626:     transform => sub {
  627:       my $str = lc(trim($_[0]));
  628:       $str =~ s/[^\w\s-]//g;  # remove special characters
  629:       $str =~ s/\s+/-/g;      # replace spaces with hyphens
  630:       return $str;
  631:     },
  632:     matches => qr/^[a-z0-9-]+$/
  633:   }
  634: 
  635:   phone => {
  636:     type => 'string',
  637:     transform => sub {
  638:       my $str = $_[0];
  639:       $str =~ s/\D//g;  # remove all non-digits
  640:       return $str;
  641:     },
  642:     matches => qr/^\d{10}$/
  643:   }
  644: 
  645: The C<transform> function is applied to the value before any validation checks (C<min>/C<minimum>, C<max>,
  646: C<matches>, C<callback>, etc.), ensuring that validation rules are checked against the cleaned data.
  647: 
  648: Transformations work with all parameter types including nested structures:
  649: 
  650:   user => {
  651:     type => 'hashref',
  652:     schema => {
  653:       name => {
  654:         type => 'string',
  655:         transform => sub { trim($_[0]) }
  656:       }, email => {
  657:         type => 'string',
  658:         transform => sub { lc(trim($_[0])) }
  659:       }
  660:     }
  661:   }
  662: 
  663: Transformations can also be defined in custom types for reusability:
  664: 
  665:   my $custom_types = {
  666:     email => {
  667:       type => 'string',
  668:       transform => sub { lc(trim($_[0])) },
  669:       matches => qr/^[\w\.\-]+@[\w\.\-]+\.\w+$/
  670:     }
  671:   };
  672: 
  673: Note that the transformed value is what gets returned in the validated result and is what
  674: subsequent validation rules will check against. If a transformation might fail, ensure it
  675: handles edge cases appropriately.
  676: It is the responsibility of the transformer to ensure that the type of the returned value is correct,
  677: since that is what will be validated.
  678: 
  679: Many validators also allow a code ref to be passed so that you can create your own, conditional validation rule, e.g.:
  680: 
  681:   $schema = {
  682:     age => {
  683:       type => 'integer',
  684:       min => sub {
  685:           my ($value, $all_params) = @_;
  686:           return $all_params->{country} eq 'US' ? 21 : 18;
  687:       }
  688:     }
  689:   }
  690: 
  691: =item * C<validator>
  692: 
  693: A synonym of C<validate>, for compatibility with L<Data::Processor>.
  694: 
  695: =item * C<cross_validation>
  696: 
  697: A reference to a hash that defines validation rules that depend on more than one parameter.
  698: Cross-field validations are performed after all individual parameter validations have passed,
  699: allowing you to enforce business logic that requires checking relationships between different fields.
  700: 
  701: Each cross-validation rule is a key-value pair where the key is a descriptive name for the validation
  702: and the value is a code reference that accepts a hash reference of all validated parameters.
  703: The subroutine should return C<undef> if the validation passes, or an error message string if it fails.
  704: 
  705:   my $schema = {
  706:     password => { type => 'string', min => 8 },
  707:     password_confirm => { type => 'string' }
  708:   };
  709: 
  710:   my $cross_validation = {
  711:     passwords_match => sub {
  712:       my $params = shift;
  713:       return $params->{password} eq $params->{password_confirm}
  714:         ? undef : "Passwords don't match";
  715:     }
  716:   };
  717: 
  718:   my $validated = validate_strict(
  719:     schema => $schema,
  720:     input => $input,
  721:     cross_validation => $cross_validation
  722:   );
  723: 
  724: Common use cases include password confirmation, date range validation, numeric comparisons,
  725: and conditional requirements:
  726: 
  727:   # Date range validation
  728:   my $cross_validation = {
  729:     date_range_valid => sub {
  730:       my $params = shift;
  731:       return $params->{start_date} le $params->{end_date}
  732:         ? undef : "Start date must be before or equal to end date";
  733:     }
  734:   };
  735: 
  736:   # Price range validation
  737:   my $cross_validation = {
  738:     price_range_valid => sub {
  739:       my $params = shift;
  740:       return $params->{min_price} <= $params->{max_price}
  741:         ? undef : "Minimum price must be less than or equal to maximum price";
  742:     }
  743:   };
  744: 
  745:   # Conditional required field
  746:   my $cross_validation = {
  747:     address_required_for_delivery => sub {
  748:       my $params = shift;
  749:       if ($params->{shipping_method} eq 'delivery' && !$params->{delivery_address}) {
  750:         return "Delivery address is required when shipping method is 'delivery'";
  751:       }
  752:       return undef;
  753:     }
  754:   };
  755: 
  756: Multiple cross-validations can be defined in the same hash, and they are all checked in order.
  757: If any cross-validation fails, the function will C<croak> with the error message returned by the validation:
  758: 
  759:   my $cross_validation = {
  760:     passwords_match => sub {
  761:       my $params = shift;
  762:       return $params->{password} eq $params->{password_confirm}
  763:         ? undef : "Passwords don't match";
  764:     },
  765:     emails_match => sub {
  766:       my $params = shift;
  767:       return $params->{email} eq $params->{email_confirm}
  768:         ? undef : "Email addresses don't match";
  769:     },
  770:     age_matches_birth_year => sub {
  771:       my $params = shift;
  772:       my $current_year = (localtime)[5] + 1900;
  773:       my $calculated_age = $current_year - $params->{birth_year};
  774:       return abs($calculated_age - $params->{age}) <= 1
  775:         ? undef : "Age doesn't match birth year";
  776:     }
  777:   };
  778: 
  779: Cross-validations receive the parameters after individual validation and transformation have been applied,
  780: so you can rely on the data being in the correct format and type:
  781: 
  782:   my $schema = {
  783:     email => {
  784:       type => 'string',
  785:       transform => sub { lc($_[0]) }  # Lowercased before cross-validation
  786:     },
  787:     email_confirm => {
  788:       type => 'string',
  789:       transform => sub { lc($_[0]) }
  790:     }
  791:   };
  792: 
  793:   my $cross_validation = {
  794:     emails_match => sub {
  795:       my $params = shift;
  796:       # Both emails are already lowercased at this point
  797:       return $params->{email} eq $params->{email_confirm}
  798:         ? undef : "Email addresses don't match";
  799:     }
  800:   };
  801: 
  802: Cross-validations can access nested structures and optional fields:
  803: 
  804:   my $cross_validation = {
  805:     guardian_required_for_minors => sub {
  806:       my $params = shift;
  807:       if ($params->{user}{age} < 18 && !$params->{guardian}) {
  808:         return "Guardian information required for users under 18";
  809:       }
  810:       return undef;
  811:     }
  812:   };
  813: 
  814: =item * metadata
  815: 
  816: Fields starting with <_> are generated by L<App::Test::Generator::SchemaExtractor>,
  817: and are currently ignored.
  818: 
  819: =item * schematic
  820: 
  821: TODO: gives an idea of what the field will be, e.g. C<filename>.
  822: 
  823: All cross-validations must pass for the overall validation to succeed.
  824: 
  825: =item * C<relationships>
  826: 
  827: A reference to an array that defines validation rules based on relationships between parameters.
  828: Relationship validations are performed after all individual parameter validations have passed,
  829: but before cross-validations.
  830: 
  831: Each relationship is a hash reference with a C<type> field and additional fields depending on the type:
  832: 
  833: =over 4
  834: 
  835: =item * B<mutually_exclusive>
  836: 
  837: Parameters that cannot be specified together.
  838: 
  839:   relationships => [
  840:     {
  841:       type => 'mutually_exclusive',
  842:       params => ['file', 'content'],
  843:       description => 'Cannot specify both file and content'
  844:     }
  845:   ]
  846: 
  847: =item * B<required_group>
  848: 
  849: At least one parameter from the group must be specified.
  850: 
  851:   relationships => [
  852:     {
  853:       type => 'required_group',
  854:       params => ['id', 'name'],
  855:       logic => 'or',
  856:       description => 'Must specify either id or name'
  857:     }
  858:   ]
  859: 
  860: =item * B<conditional_requirement>
  861: 
  862: If one parameter is specified, another becomes required.
  863: 
  864:   relationships => [
  865:     {
  866:       type => 'conditional_requirement',
  867:       if => 'async',
  868:       then_required => 'callback',
  869:       description => 'When async is specified, callback is required'
  870:     }
  871:   ]
  872: 
  873: =item * B<dependency>
  874: 
  875: One parameter requires another to be present.
  876: 
  877:   relationships => [
  878:     {
  879:       type => 'dependency',
  880:       param => 'port',
  881:       requires => 'host',
  882:       description => 'port requires host to be specified'
  883:     }
  884:   ]
  885: 
  886: =item * B<value_constraint>
  887: 
  888: Specific value requirements between parameters.
  889: 
  890:   relationships => [
  891:     {
  892:       type => 'value_constraint',
  893:       if => 'ssl',
  894:       then => 'port',
  895:       operator => '==',
  896:       value => 443,
  897:       description => 'When ssl is specified, port must equal 443'
  898:     }
  899:   ]
  900: 
  901: =item * B<value_conditional>
  902: 
  903: Parameter required when another has a specific value.
  904: 
  905:   relationships => [
  906:     {
  907:       type => 'value_conditional',
  908:       if => 'mode',
  909:       equals => 'secure',
  910:       then_required => 'key',
  911:       description => "When mode equals 'secure', key is required"
  912:     }
  913:   ]
  914: 
  915: =back
  916: 
  917: If a parameter is optional and its value is C<undef>,
  918: validation will be skipped for that parameter.
  919: 
  920: If the validation fails, the function will C<croak> with an error message describing the validation failure.
  921: 
  922: If the validation is successful, the function will return a reference to a new hash containing the validated and (where applicable) coerced parameters.  Integer and number parameters will be coerced to their respective types.
  923: 
  924: The C<description> field is optional but recommended for clearer error messages.
  925: 
  926: =back
  927: 
  928: =head2 Example Usage
  929: 
  930:   my $schema = {
  931:     host => { type => 'string' },
  932:     port => { type => 'integer' },
  933:     ssl => { type => 'boolean' },
  934:     file => { type => 'string', optional => 1 },
  935:     content => { type => 'string', optional => 1 }
  936:   };
  937: 
  938:   my $relationships = [
  939:     {
  940:       type => 'mutually_exclusive',
  941:       params => ['file', 'content']
  942:     },
  943:     {
  944:       type => 'required_group',
  945:       params => ['host', 'file']
  946:     },
  947:     {
  948:       type => 'dependency',
  949:       param => 'port',
  950:       requires => 'host'
  951:     },
  952:     {
  953:       type => 'value_constraint',
  954:       if => 'ssl',
  955:       then => 'port',
  956:       operator => '==',
  957:       value => 443
  958:     }
  959:   ];
  960: 
  961:   my $validated = validate_strict(
  962:     schema => $schema,
  963:     input => $input,
  964:     relationships => $relationships
  965:   );
  966: 
  967: =head1 MIGRATION FROM LEGACY VALIDATORS
  968: 
  969: =head2 From L<Params::Validate>
  970: 
  971:     # Old style
  972:     validate(@_, {
  973:         name => { type => SCALAR },
  974:         age => { type => SCALAR, regex => qr/^\d+$/ }
  975:     });
  976: 
  977:     # New style
  978:     validate_strict(
  979:         schema => {	# or "members"
  980:             name => 'string',
  981:             age => { type => 'integer', min => 0 }
  982:         },
  983:         args => { @_ }
  984:     );
  985: 
  986: =head2 From L<Type::Params>
  987: 
  988:     # Old style
  989:     my ($name, $age) = validate_positional \@_, Str, Int;
  990: 
  991:     # New style - requires converting to named parameters first
  992:     my %args = (name => $_[0], age => $_[1]);
  993:     my $validated = validate_strict(
  994:         schema => { name => 'string', age => 'integer' },
  995:         args => \%args
  996:     );
  997: 
  998: =cut
  999: 
 1000: sub validate_strict
 1001: {
1002 → 1009 → 10171002 → 1009 → 0 1002: 	my $params = Params::Get::get_params(undef, \@_);
 1003: 
 1004: 	my $schema = $params->{'schema'} || $params->{'members'};
 1005: 	my $args = $params->{'args'} || $params->{'input'};
 1006: 	my $logger = $params->{'logger'};
 1007: 	my $custom_types = $params->{'custom_types'};
 1008: 	my $unknown_parameter_handler = $params->{'unknown_parameter_handler'};
 1009: 	if(!defined($unknown_parameter_handler)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1010: if($params->{'carp_on_warn'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1011: $unknown_parameter_handler = 'warn'; 1012: } else { 1013: $unknown_parameter_handler = 'die'; 1014: } 1015: } 1016: 1017 → 1021 → 10261017 → 1021 → 0 1017: return $args if(!defined($schema)); # No schema, allow all arguments

Mutants (Total: 2, Killed: 2, Survived: 0)

1018: 1019: # Accept arrayref schema: [{ name=>'param', type=>'...', ... }, ...] 1020: # Normalise to the standard named-parameter hashref form before further processing. 1021: if(ref($schema) eq 'ARRAY') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1022: $schema = _schema_from_arrayref($schema, $logger); 1023: } 1024: 1025: # Check if schema and args are references to hashes 1026 → 1026 → 10311026 → 1026 → 0 1026: if(ref($schema) ne 'HASH') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1027: _error($logger, 'validate_strict: schema must be a hash reference'); 1028: } 1029: 1030: # Inspired by Data::Processor 1031 → 1034 → 10441031 → 1034 → 0 1031: my $schema_description = $params->{'description'} || 'validate_strict'; 1032: my $error_msg = $params->{'error_msg'}; 1033: 1034: if($schema->{'members'} && ($schema->{'description'} || $schema->{'error_msg'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1035: $schema_description = $schema->{'description'}; 1036: $error_msg = $schema->{'error_msg'}; 1037: $schema = $schema->{'members'}; 1038: # The members value may also be in arrayref form 1039: if(ref($schema) eq 'ARRAY') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1040: $schema = _schema_from_arrayref($schema, $logger); 1041: } 1042: } 1043: 1044 → 1044 → 10501044 → 1044 → 0 1044: if(exists($params->{'args'}) && (!defined($args))) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1045: $args = {}; 1046: } elsif((ref($args) ne 'HASH') && (ref($args) ne 'ARRAY')) { 1047: _error($logger, $error_msg || "$schema_description: args must be a hash or array reference"); 1048: } 1049: 1050 → 1050 → 10721050 → 1050 → 0 1050: if(ref($args) eq 'HASH') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1051: # Named args 1052: foreach my $key (keys %{$args}) { 1053: if(!exists($schema->{$key})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1054: if($unknown_parameter_handler eq 'die') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1055: _error($logger, "$schema_description: Unknown parameter '$key'"); 1056: } elsif($unknown_parameter_handler eq 'warn') { 1057: _warn($logger, "$schema_description: Unknown parameter '$key'"); 1058: next; 1059: } elsif($unknown_parameter_handler eq 'ignore') { 1060: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1061: $logger->debug(__PACKAGE__ . ": $schema_description: Unknown parameter '$key'"); 1062: } 1063: next; 1064: } else { 1065: _error($logger, "$schema_description: '$unknown_parameter_handler' unknown_parameter_handler must be one of die, warn, ignore"); 1066: } 1067: } 1068: } 1069: } 1070: 1071: # Find out if this routine takes positional arguments 1072 → 1073 → 10941072 → 1073 → 0 1072: my $are_positional_args = -1; 1073: foreach my $key (keys %{$schema}) { 1074: if(defined(my $rules = $schema->{$key})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1075: if(ref($rules) eq 'HASH') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1076: if(!defined($rules->{'position'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1077: if($are_positional_args == 1) {

Mutants (Total: 2, Killed: 2, Survived: 0)

1078: _error($logger, "::validate_strict: $key is missing position value"); 1079: } 1080: $are_positional_args = 0; 1081: last; 1082: } 1083: $are_positional_args = 1; 1084: } else { 1085: $are_positional_args = 0; 1086: last; 1087: } 1088: } else { 1089: $are_positional_args = 0; 1090: last; 1091: } 1092: } 1093: 1094 → 1096 → 18551094 → 1096 → 0 1094: my %validated_args; 1095: my %invalid_args; 1096: foreach my $key (keys %{$schema}) { 1097: my $rules = $schema->{$key}; 1098: my $value; 1099: if($are_positional_args == 1) {

Mutants (Total: 2, Killed: 2, Survived: 0)

1100: if(ref($args) ne 'ARRAY') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1101: _error($logger, "::validate_strict: position $rules->{position} given for '$key', but args isn't an array"); 1102: } 1103: $value = @{$args}[$rules->{'position'}]; 1104: } else { 1105: $value = $args->{$key}; 1106: } 1107: 1108: if(!defined($rules)) { # Allow anything

Mutants (Total: 1, Killed: 1, Survived: 0)

1109: $validated_args{$key} = $value; 1110: next; 1111: } 1112: 1113: # If rules are a simple type string 1114: if(ref($rules) eq '') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1115: $rules = { type => $rules }; 1116: } 1117: 1118: my $is_optional = 0; 1119: 1120: my $rule_description = $schema_description; # Can be overridden in each element 1121: 1122: if(ref($rules) eq 'HASH') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1123: if(exists($rules->{'description'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1124: $rule_description = $rules->{'description'}; 1125: } 1126: if($rules->{'transform'} && defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1127: if(ref($rules->{'transform'}) eq 'CODE') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1128: $value = &{$rules->{'transform'}}($value); 1129: } else { 1130: _error($logger, "$rule_description: transforms must be a code ref"); 1131: } 1132: } 1133: if(exists($rules->{optional})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1134: if(ref($rules->{'optional'}) eq 'CODE') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1135: $is_optional = &{$rules->{optional}}($value, $args); 1136: } else { 1137: $is_optional = $rules->{'optional'}; 1138: } 1139: } elsif($rules->{nullable}) { 1140: $is_optional = $rules->{'nullable'}; 1141: } 1142: } 1143: 1144: # Handle optional parameters 1145: if((ref($rules) eq 'HASH') && $is_optional) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1146: my $look_for_default = 0; 1147: if($are_positional_args == 1) {

Mutants (Total: 2, Killed: 2, Survived: 0)

1148: # if(!defined(@{$args}[$rules->{'position'}])) { 1149: if(!defined($args->[$rules->{position}])) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1150: $look_for_default = 1; 1151: } 1152: } else { 1153: if(!exists($args->{$key})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1154: $look_for_default = 1; 1155: } 1156: } 1157: if($look_for_default) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1158: if($are_positional_args == 1) {

Mutants (Total: 2, Killed: 2, Survived: 0)

1159: if(scalar(@{$args}) < $rules->{'position'}) {

Mutants (Total: 4, Killed: 1, Survived: 3)
1160: # arg array is too short, so it must be missing 1161: _error($logger, "$rule_description: Required parameter '$key' is missing"); 1162: next; 1163: } 1164: } 1165: if(exists($rules->{'default'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1166: # Populate missing optional parameters with the specified output values 1167: $validated_args{$key} = $rules->{'default'}; 1168: next; # default wins; do not fall through to the schema branch 1169: } 1170: 1171: if($rules->{'schema'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1172: $value = _apply_nested_defaults({}, $rules->{'schema'}); 1173: next unless scalar(%{$value}); 1174: # The nested schema has a default value 1175: } else { 1176: next; # optional and missing 1177: } 1178: } 1179: } elsif((ref($args) eq 'HASH') && !exists($args->{$key})) { 1180: # The parameter is required 1181: # Use exists rather than defined, so that an undefined value can be passed, but the key is there 1182: _error($logger, "$rule_description: Required parameter '$key' is missing"); 1183: } 1184: 1185: # Normalise union type shorthand: { type => ['string', 'integer'], ... } 1186: # into the array-of-rules form that the ARRAY handler below already supports. 1187: # Each candidate type inherits all other constraints from the parent rule 1188: # (min, max, matches, optional, etc.) so they are each fully validated. 1189: # Must run after optional/transform handling above but before rule dispatch below. 1190: if(ref($rules) eq 'HASH' && ref($rules->{'type'}) eq 'ARRAY') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1191: my %base = %{$rules}; 1192: my @type_list = @{delete $base{'type'}}; 1193: if(!@type_list) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1194: _error($logger, "$rule_description: Parameter '$key': union type list must not be empty"); 1195: } 1196: # Expand into one full rule hash per candidate type 1197: $rules = [ map { { %base, type => $_ } } @type_list ]; 1198: } 1199: 1200: # Validate based on rules 1201: if(ref($rules) eq 'HASH') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1202: if(defined(my $min = $rules->{'min'} // $rules->{'minimum'}) && defined(my $max = $rules->{'max'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1203: if($min > $max) {

Mutants (Total: 4, Killed: 4, Survived: 0)

1204: _error($logger, "validate_strict($key): min must be <= max ($min > $max)"); 1205: } 1206: } 1207: 1208: # memberof and its synonym enum cannot be combined with min or max 1209: if($rules->{'memberof'} || $rules->{'enum'} || $rules->{'values'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1210: if(defined(my $min = $rules->{'min'} // $rules->{'minimum'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1211: _error($logger, "validate_strict($key): min ($min) makes no sense with memberof/enum/values"); 1212: } 1213: if(defined(my $max = $rules->{'max'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1214: _error($logger, "validate_strict($key): max ($max) makes no sense with memberof/enum/values"); 1215: } 1216: } 1217: 1218: foreach my $rule_name (keys %$rules) { 1219: my $rule_value = $rules->{$rule_name}; 1220: 1221: if((ref($rule_value) eq 'CODE')

Mutants (Total: 1, Killed: 1, Survived: 0)

1222: && ($rule_name ne 'validate') 1223: && ($rule_name ne 'callback') 1224: && ($rule_name ne 'validator') 1225: && ($rule_name ne 'transform') # already applied before this loop 1226: && ($rule_name ne 'optional')) { # already applied before this loop 1227: $rule_value = &{$rule_value}($value, $args); 1228: } 1229: 1230: # Better OOP, the routine has been given an object rather than a scalar 1231: if(Scalar::Util::blessed($rule_value) && $rule_value->can('as_string')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1232: $rule_value = $rule_value->as_string(); 1233: } 1234: 1235: if($rule_name eq 'type') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1236: my $type = lc($rule_value); 1237: 1238: if(($type eq 'string') || ($type eq 'str')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1239: if(ref($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1240: _error($logger, $rules->{'error_msg'} || "$rule_description: Parameter '$key' must be a string"); 1241: } 1242: unless((ref($value) eq '') || (defined($value) && length($value))) { # Allow undef for optional strings

Mutants (Total: 1, Killed: 1, Survived: 0)

1243: _error($logger, $rules->{'error_msg'} || "$rule_description: Parameter '$key' must be a string"); 1244: } 1245: } elsif(($type eq 'integer') || ($type eq 'int')) { 1246: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1247: next; # Skip if number is undefined 1248: } 1249: if($value !~ /^\s*[+\-]?\d+\s*$/) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1250: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1251: _error($logger, $rules->{'error_msg'}); 1252: } else { 1253: _error($logger, "$rule_description: Parameter '$key' ($value) must be an integer"); 1254: } 1255: } 1256: $value = int($value); # Coerce to integer 1257: } elsif(($type eq 'number') || ($type eq 'float') || ($type eq 'num') || ($type eq 'double')) { 1258: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1259: next; # Skip if number is undefined 1260: } 1261: if(!Scalar::Util::looks_like_number($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1262: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1263: _error($logger, $rules->{'error_msg'}); 1264: } else { 1265: _error($logger, "$rule_description: Parameter '$key' must be a number"); 1266: } 1267: } 1268: # $value = eval $value; # Coerce to number (be careful with eval) 1269: $value = 0 + $value; # Numeric coercion 1270: } elsif($type eq 'arrayref') { 1271: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1272: next; # Skip if arrayref is undefined 1273: } 1274: if(ref($value) ne 'ARRAY') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1275: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1276: _error($logger, $rules->{'error_msg'}); 1277: } else { 1278: _error($logger, "$rule_description: Parameter '$key' must be an arrayref, not " . ref($value)); 1279: } 1280: } 1281: } elsif($type eq 'hashref') { 1282: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1283: next; # Skip if hashref is undefined 1284: } 1285: if(ref($value) ne 'HASH') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1286: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1287: _error($logger, $rules->{'error_msg'}); 1288: } else { 1289: _error($logger, "$rule_description: Parameter '$key' must be an hashref"); 1290: } 1291: } 1292: } elsif(($type eq 'boolean') || ($type eq 'bool')) { 1293: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1294: next; # Skip if bool is undefined 1295: } 1296: if(defined(my $b = $Readonly::Values::Boolean::booleans{$value})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1297: $value = $b; 1298: } else { 1299: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1300: _error($logger, $rules->{'error_msg'}); 1301: } else { 1302: _error($logger, "$rule_description: Parameter '$key' ($value) must be a boolean"); 1303: } 1304: } 1305: } elsif($type eq 'coderef') { 1306: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1307: next; # Skip if code is undefined 1308: } 1309: if(ref($value) ne 'CODE') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1310: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1311: _error($logger, $rules->{'error_msg'}); 1312: } else { 1313: _error($logger, "$rule_description: Parameter '$key' must be a coderef, not a ref to " . ref($value)); 1314: } 1315: } 1316: } elsif($type eq 'object') { 1317: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1318: next; # Skip if object is undefined 1319: } 1320: if(!Scalar::Util::blessed($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1321: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1322: _error($logger, $rules->{'error_msg'}); 1323: } else { 1324: _error($logger, "$rule_description: Parameter '$key' must be an object"); 1325: } 1326: } 1327: } elsif(my $custom_type = $custom_types->{$type}) { 1328: if($custom_type->{'transform'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1329: # The custom type has a transform embedded within it 1330: if(ref($custom_type->{'transform'}) eq 'CODE') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1331: $value = &{$custom_type->{'transform'}}($value); 1332: } else { 1333: _error($logger, "$rule_description: transforms must be a code ref"); 1334: next; 1335: } 1336: } 1337: validate_strict({ input => { $key => $value }, schema => { $key => $custom_type }, custom_types => $custom_types }); 1338: } else { 1339: _error($logger, "$rule_description: Unknown type '$type'"); 1340: } 1341: } elsif(($rule_name eq 'min') || ($rule_name eq 'minimum')) { 1342: if(!defined($rules->{'type'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1343: _error($logger, "$rule_description: Don't know type of '$key' to determine its minimum value $rule_value"); 1344: } 1345: my $type = lc($rules->{'type'}); 1346: if(exists($custom_types->{$type}->{'min'}) || exists($custom_types->{$type}->{minimum})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1347: $rule_value = $custom_types->{$type}->{'min'} // $custom_types->{$type}->{minumum}; 1348: $type = $custom_types->{$type}->{'type'}; 1349: } 1350: if(($type eq 'string') || ($type eq 'str')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1351: if($rule_value < 0) {

Mutants (Total: 4, Killed: 4, Survived: 0)

1352: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1353: _error($logger, $rules->{'error_msg'}); 1354: } else { 1355: _error($logger, "$rule_description: String parameter '$key' has meaningless minimum value that is less than zero"); 1356: } 1357: } 1358: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1359: next; # Skip if string is undefined 1360: } 1361: if(defined(my $len = _number_of_characters($value))) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1362: if($len < $rule_value) {

Mutants (Total: 4, Killed: 4, Survived: 0)

1363: _error($logger, $rules->{'error_msg'} || "$rule_description: String parameter '$key' too short, ($len characters), must be at least $rule_value characters"); 1364: $invalid_args{$key} = 1; 1365: } 1366: } else { 1367: _error($logger, $rules->{'error_msg'} || "$rule_description: '$key' can't be decoded"); 1368: $invalid_args{$key} = 1; 1369: } 1370: } elsif($type eq 'arrayref') { 1371: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1372: next; # Skip if array is undefined 1373: } 1374: if(ref($value) ne 'ARRAY') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1375: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1376: _error($logger, $rules->{'error_msg'}); 1377: } else { 1378: _error($logger, "$rule_description: Parameter '$key' must be an arrayref, not " . ref($value)); 1379: } 1380: } 1381: if(scalar(@{$value}) < $rule_value) {

Mutants (Total: 4, Killed: 4, Survived: 0)

1382: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1383: _error($logger, $rules->{'error_msg'}); 1384: } else { 1385: _error($logger, "$rule_description: Parameter '$key' must be at least length $rule_value"); 1386: } 1387: $invalid_args{$key} = 1; 1388: } 1389: } elsif($type eq 'hashref') { 1390: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1391: next; # Skip if hash is undefined 1392: } 1393: if(scalar(keys(%{$value})) < $rule_value) {

Mutants (Total: 4, Killed: 4, Survived: 0)

1394: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1395: _error($logger, $rules->{'error_msg'}); 1396: } else { 1397: _error($logger, "$rule_description: Parameter '$key' must contain at least $rule_value keys"); 1398: } 1399: $invalid_args{$key} = 1; 1400: } 1401: } elsif(($type eq 'integer') || ($type eq 'number') || ($type eq 'float')) { 1402: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1403: next; # Skip if hash is undefined 1404: } 1405: if(Scalar::Util::looks_like_number($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1406: if($value < $rule_value) {

Mutants (Total: 4, Killed: 4, Survived: 0)

1407: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1408: _error($logger, $rules->{'error_msg'}); 1409: } else { 1410: _error($logger, "$rule_description: Parameter '$key' ($value) must be at least $rule_value"); 1411: } 1412: $invalid_args{$key} = 1; 1413: next; 1414: } 1415: } else { 1416: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1417: _error($logger, $rules->{'error_msg'}); 1418: } else { 1419: _error($logger, "$rule_description: Parameter '$key' ($value) must be a number"); 1420: } 1421: next; 1422: } 1423: } else { 1424: _error($logger, "$rule_description: Parameter '$key' of type '$type' has meaningless min value $rule_value"); 1425: } 1426: } elsif($rule_name eq 'max') { 1427: if(!defined($rules->{'type'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1428: _error($logger, "$rule_description: Don't know type of '$key' to determine its maximum value $rule_value"); 1429: } 1430: my $type = lc($rules->{'type'}); 1431: if(exists($custom_types->{$type}->{'max'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1432: $rule_value = $custom_types->{$type}->{'max'}; 1433: $type = $custom_types->{$type}->{'type'}; 1434: } 1435: if(($type eq 'string') || ($type eq 'str')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1436: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1437: next; # Skip if string is undefined 1438: } 1439: if(defined(my $len = _number_of_characters($value))) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1440: if($len > $rule_value) {

Mutants (Total: 4, Killed: 4, Survived: 0)

1441: _error($logger, $rules->{'error_msg'} || "$rule_description: String parameter '$key' too long, ($len characters), must be no longer than $rule_value"); 1442: $invalid_args{$key} = 1; 1443: } 1444: } else { 1445: _error($logger, $rules->{'error_msg'} || "$rule_description: '$key' can't be decoded"); 1446: $invalid_args{$key} = 1; 1447: } 1448: } elsif($type eq 'arrayref') { 1449: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1450: next; # Skip if string is undefined 1451: } 1452: if(ref($value) ne 'ARRAY') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1453: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1454: _error($logger, $rules->{'error_msg'}); 1455: } else { 1456: _error($logger, "$rule_description: Parameter '$key' must be an arrayref, not " . ref($value)); 1457: } 1458: } 1459: if(scalar(@{$value}) > $rule_value) {

Mutants (Total: 4, Killed: 4, Survived: 0)

1460: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1461: _error($logger, $rules->{'error_msg'}); 1462: } else { 1463: _error($logger, "$rule_description: Parameter '$key' must contain no more than $rule_value items"); 1464: } 1465: $invalid_args{$key} = 1; 1466: } 1467: } elsif($type eq 'hashref') { 1468: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1469: next; # Skip if hash is undefined 1470: } 1471: if(scalar(keys(%{$value})) > $rule_value) {

Mutants (Total: 4, Killed: 4, Survived: 0)

1472: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1473: _error($logger, $rules->{'error_msg'}); 1474: } else { 1475: _error($logger, "$rule_description: Parameter '$key' must contain no more than $rule_value keys"); 1476: } 1477: $invalid_args{$key} = 1; 1478: } 1479: } elsif(($type eq 'integer') || ($type eq 'number') || ($type eq 'float')) { 1480: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1481: next; # Skip if hash is undefined 1482: } 1483: if(Scalar::Util::looks_like_number($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1484: if($value > $rule_value) {

Mutants (Total: 4, Killed: 4, Survived: 0)

1485: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1486: _error($logger, $rules->{'error_msg'}); 1487: } else { 1488: _error($logger, "$rule_description: Parameter '$key' ($value) must be no more than $rule_value"); 1489: } 1490: $invalid_args{$key} = 1; 1491: next; 1492: } 1493: } else { 1494: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1495: _error($logger, $rules->{'error_msg'}); 1496: } else { 1497: _error($logger, "$rule_description: Parameter '$key' ($value) must be a number"); 1498: } 1499: next; 1500: } 1501: } else { 1502: _error($logger, "$rule_description: Parameter '$key' of type '$type' has meaningless max value $rule_value"); 1503: } 1504: } elsif(($rule_name eq 'matches') || ($rule_name eq 'regex')) { 1505: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1506: next; # Skip if string is undefined 1507: } 1508: eval { 1509: my $re = (ref($rule_value) eq 'Regexp') ? $rule_value : qr/\Q$rule_value\E/; 1510: if(($rules->{'type'} eq 'arrayref') || ($rules->{'type'} eq 'ArrayRef')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1511: my @matches = grep { $_ =~ $re } @{$value}; 1512: if(scalar(@matches) != scalar(@{$value})) {

Mutants (Total: 2, Killed: 2, Survived: 0)

1513: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1514: _error($logger, $rules->{'error_msg'}); 1515: } else { 1516: _error($logger, "$rule_description: All members of parameter '$key' [", join(', ', @{$value}), "] must match pattern '$rule_value'"); 1517: } 1518: } 1519: } elsif($value !~ $re) { 1520: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1521: _error($logger, $rules->{'error_msg'}); 1522: } else { 1523: _error($logger, "$rule_description: Parameter '$key' ($value) must match pattern '$re'"); 1524: } 1525: } 1526: 1; 1527: }; 1528: if($@) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1529: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1530: _error($logger, $rules->{'error_msg'}); 1531: } else { 1532: _error($logger, "$rule_description: Parameter '$key' regex '$rule_value' error: $@"); 1533: } 1534: $invalid_args{$key} = 1; 1535: } 1536: } elsif($rule_name eq 'nomatch') { 1537: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1538: next; # Skip if string is undefined 1539: } 1540: if(($rules->{'type'} eq 'arrayref') || ($rules->{'type'} eq 'ArrayRef')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1541: my @matches = grep { /$rule_value/ } @{$value}; 1542: if(scalar(@matches)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1543: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1544: _error($logger, $rules->{'error_msg'}); 1545: } else { 1546: _error($logger, "$rule_description: No member of parameter '$key' [", join(', ', @{$value}), "] must match pattern '$rule_value'"); 1547: } 1548: } 1549: } elsif($value =~ $rule_value) { 1550: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1551: _error($logger, $rules->{'error_msg'}); 1552: } else { 1553: _error($logger, "$rule_description: Parameter '$key' ($value) must not match pattern '$rule_value'"); 1554: } 1555: $invalid_args{$key} = 1; 1556: } 1557: } elsif(($rule_name eq 'memberof') || ($rule_name eq 'enum') || ($rule_name eq 'values')) { 1558: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1559: next; # Skip if string is undefined 1560: } 1561: if(ref($rule_value) eq 'ARRAY') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1562: my $ok = 1; 1563: if(($rules->{'type'} eq 'integer') || ($rules->{'type'} eq 'number') || ($rules->{'type'} eq 'float')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1564: unless(List::Util::any { $_ == $value } @{$rule_value}) {

Mutants (Total: 2, Killed: 2, Survived: 0)

1565: $ok = 0; 1566: } 1567: } else { 1568: my $l = lc($value); 1569: unless(List::Util::any { (!defined($rules->{'case_sensitive'}) || ($rules->{'case_sensitive'} == 1)) ? $_ eq $value : lc($_) eq $l } @{$rule_value}) {

Mutants (Total: 2, Killed: 2, Survived: 0)

1570: $ok = 0; 1571: } 1572: } 1573: 1574: if(!$ok) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1575: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1576: _error($logger, $rules->{'error_msg'}); 1577: } else { 1578: _error($logger, "$rule_description: Parameter '$key' ($value) must be one of ", join(', ', @{$rule_value})); 1579: } 1580: $invalid_args{$key} = 1; 1581: } 1582: } else { 1583: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1584: _error($logger, $rules->{'error_msg'}); 1585: } else { 1586: _error($logger, "$rule_description: Parameter '$key' rule ($rule_value) must be an array reference"); 1587: } 1588: } 1589: } elsif($rule_name eq 'notmemberof') { 1590: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1591: next; # Skip if string is undefined 1592: } 1593: if(ref($rule_value) eq 'ARRAY') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1594: my $ok = 1; 1595: if(($rules->{'type'} eq 'integer') || ($rules->{'type'} eq 'number') || ($rules->{'type'} eq 'float')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1596: if(List::Util::any { $_ == $value } @{$rule_value}) {

Mutants (Total: 2, Killed: 2, Survived: 0)

1597: $ok = 0; 1598: } 1599: } else { 1600: my $l = lc($value); 1601: if(List::Util::any { (!defined($rules->{'case_sensitive'}) || ($rules->{'case_sensitive'} == 1)) ? $_ eq $value : lc($_) eq $l } @{$rule_value}) {

Mutants (Total: 2, Killed: 2, Survived: 0)

1602: $ok = 0; 1603: } 1604: } 1605: 1606: if(!$ok) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1607: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1608: _error($logger, $rules->{'error_msg'}); 1609: } else { 1610: _error($logger, "$rule_description: Parameter '$key' ($value) must not be one of ", join(', ', @{$rule_value})); 1611: } 1612: $invalid_args{$key} = 1; 1613: } 1614: } else { 1615: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1616: _error($logger, $rules->{'error_msg'}); 1617: } else { 1618: _error($logger, "$rule_description: Parameter '$key' rule ($rule_value) must be an array reference"); 1619: } 1620: } 1621: } elsif($rule_name eq 'isa') { 1622: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1623: next; # Skip if object not given 1624: } 1625: if($rules->{'type'} eq 'object') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1626: if(!$value->isa($rule_value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1627: _error($logger, "$rule_description: Parameter '$key' must be a '$rule_value' object got a " . (ref($value) ? ref($value) : $value) . ' object instead'); 1628: $invalid_args{$key} = 1; 1629: } 1630: } else { 1631: _error($logger, "$rule_description: Parameter '$key' has meaningless isa value $rule_value"); 1632: } 1633: } elsif($rule_name eq 'can') { 1634: if(!defined($value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1635: next; # Skip if object not given 1636: } 1637: if($rules->{'type'} eq 'object') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1638: if(ref($rule_value) eq 'ARRAY') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1639: # List of methods 1640: foreach my $method(@{$rule_value}) { 1641: if(!$value->can($method)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1642: _error($logger, "$rule_description: Parameter '$key' must be an object that understands the $method method"); 1643: $invalid_args{$key} = 1; 1644: } 1645: } 1646: } elsif(!ref($rule_value)) { 1647: if(!$value->can($rule_value)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1648: _error($logger, "$rule_description: Parameter '$key' must be an object that understands the $rule_value method"); 1649: $invalid_args{$key} = 1; 1650: } 1651: } else { 1652: _error($logger, "$rule_description: 'can' rule for Parameter '$key must be either a scalar or an arrayref"); 1653: } 1654: } else { 1655: _error($logger, "$rule_description: Parameter '$key' has meaningless can value '$rule_value' for parameter type $rules->{type}"); 1656: } 1657: } elsif($rule_name eq 'element_type') { 1658: if(($rules->{'type'} eq 'arrayref') || ($rules->{'type'} eq 'ArrayRef')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1659: my $type = $rule_value; 1660: my $custom_type = $custom_types->{$rule_value}; 1661: if($custom_type && $custom_type->{'type'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1662: $type = $custom_type->{'type'}; 1663: } 1664: foreach my $member(@{$value}) { 1665: if($custom_type && $custom_type->{'transform'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1666: # The custom type has a transform embedded within it 1667: if(ref($custom_type->{'transform'}) eq 'CODE') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1668: $member = &{$custom_type->{'transform'}}($member); 1669: } else { 1670: _error($logger, "$rule_description: transforms must be a code ref"); 1671: last; 1672: } 1673: } 1674: if(($type eq 'string') || ($type eq 'Str')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1675: if(ref($member)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1676: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1677: _error($logger, $rules->{'error_msg'}); 1678: } else { 1679: _error($logger, "$key can only contain strings"); 1680: } 1681: $invalid_args{$key} = 1; 1682: } 1683: } elsif($type eq 'integer') { 1684: if(ref($member) || ($member =~ /\D/)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1685: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1686: _error($logger, $rules->{'error_msg'}); 1687: } else { 1688: _error($logger, "$key can only contain integers (found $member)"); 1689: } 1690: $invalid_args{$key} = 1; 1691: } 1692: } elsif(($type eq 'number') || ($rule_value eq 'float')) { 1693: if(ref($member) || ($member !~ /^[-+]?(\d*\.\d+|\d+\.?\d*)$/)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1694: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1695: _error($logger, $rules->{'error_msg'}); 1696: } else { 1697: _error($logger, "$key can only contain numbers (found $member)"); 1698: } 1699: $invalid_args{$key} = 1; 1700: } 1701: } else { 1702: _error($logger, "BUG: Add $type to element_type list"); 1703: } 1704: } 1705: } else { 1706: _error($logger, "$rule_description: Parameter '$key' has meaningless element_type value $rule_value"); 1707: } 1708: } elsif($rule_name eq 'optional') { 1709: # Already handled at the beginning of the loop 1710: } elsif($rule_name eq 'nullable') { 1711: # Already handled at the beginning of the loop (same as optional) 1712: } elsif($rule_name eq 'default') { 1713: # Handled earlier 1714: } elsif($rule_name eq 'error_msg') { 1715: # Handled inline 1716: } elsif($rule_name eq 'transform') { 1717: # Handled before the loop 1718: } elsif($rule_name eq 'case_sensitive') { 1719: # Handled inline 1720: } elsif($rule_name eq 'description') { 1721: # A la, Data::Processor 1722: } elsif($rule_name =~ /^_/) { 1723: # Ignore internal/metadata fields from schema extraction 1724: } elsif($rule_name eq 'semantic') { 1725: if($rule_value eq 'unix_timestamp') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1726: if($value < 0 || $value > 2147483647) {

Mutants (Total: 7, Killed: 7, Survived: 0)

1727: error($logger, 'Invalid Unix timestamp: $value'); 1728: } 1729: } else { 1730: _warn($logger, "semantic type $rule_value is not yet supported"); 1731: } 1732: } elsif($rule_name eq 'schema') { 1733: # Nested schema Run the given schema against each element of the array 1734: if(($rules->{'type'} eq 'arrayref') || ($rules->{'type'} eq 'ArrayRef')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1735: if(ref($value) eq 'ARRAY') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1736: foreach my $member(@{$value}) { 1737: # Distinguish two schema forms: 1738: # (a) Rule hash — has a top-level 'type' key, e.g. { type=>'string', matches=>qr/.../ } 1739: # → validate each element against that rule directly. 1740: # (b) Field-schema hash — keys are field names whose values are rule hashes, 1741: # e.g. { name=>{type=>'string'}, age=>{type=>'integer'} } 1742: # → validate each hashref element against the field schema directly. 1743: my $is_field_schema = (ref($rule_value) eq 'HASH') && !exists($rule_value->{'type'}); 1744: my %inner = (custom_types => $custom_types); 1745: if($is_field_schema) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1746: $inner{input} = $member; 1747: $inner{schema} = $rule_value; 1748: } else { 1749: $inner{input} = { $key => $member }; 1750: $inner{schema} = { $key => $rule_value }; 1751: } 1752: if(!validate_strict(\%inner)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1753: $invalid_args{$key} = 1; 1754: } 1755: } 1756: } elsif(defined($value)) { # Allow undef for optional values 1757: _error($logger, "$rule_description: nested schema: Parameter '$value' must be an arrayref"); 1758: } 1759: } elsif($rules->{'type'} eq 'hashref') { 1760: if(ref($rule_value) eq 'HASH') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1761: # Apply nested defaults before validation 1762: my $nested_with_defaults = _apply_nested_defaults($value, $rule_value); 1763: if(scalar keys(%{$nested_with_defaults})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1764: if(my $new_args = validate_strict({ input => $nested_with_defaults, schema => $rule_value, custom_types => $custom_types })) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1765: $value = $new_args; 1766: } else { 1767: $invalid_args{$key} = 1; 1768: } 1769: } 1770: } else { 1771: _error($logger, "$rule_description: nested schema: Parameter '$value' must be an hashref"); 1772: } 1773: } else { 1774: _error($logger, "$rule_description: Parameter '$key': 'schema' only supports arrayref and hashref, not $rules->{type}"); 1775: } 1776: } elsif(($rule_name eq 'validate') || ($rule_name eq 'validator')) { 1777: if(ref($rule_value) eq 'CODE') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1778: if(my $error = &{$rule_value}($args)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1779: _error($logger, "$rule_description: $key not valid: $error"); 1780: $invalid_args{$key} = 1; 1781: } 1782: } else { 1783: # _error($logger, "$rule_description: Parameter '$key': 'validate' only supports coderef, not $value"); 1784: _error($logger, "$rule_description: Parameter '$key': 'validate' only supports coderef, not " . ref($rule_value) // $rule_value); 1785: } 1786: } elsif ($rule_name eq 'callback') { 1787: # Custom validation code 1788: unless (defined &$rule_value) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1789: _error($logger, "$rule_description: callback for '$key' must be a code reference"); 1790: } 1791: my $res = $rule_value->($value, $args, $schema); 1792: unless ($res) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1793: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1794: _error($logger, $rules->{'error_msg'}); 1795: } else { 1796: _error($logger, "$rule_description: Parameter '$key' failed custom validation"); 1797: } 1798: $invalid_args{$key} = 1; 1799: } 1800: } elsif($rule_name eq 'position') { 1801: if($rule_value =~ /\D/) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1802: _error($logger, "$rule_description: Parameter '$key': 'position' must be an integer"); 1803: } 1804: if($rule_value < 0) {

Mutants (Total: 4, Killed: 4, Survived: 0)

1805: _error($logger, "$rule_description: Parameter '$key': 'position' must be a positive integer, not $value"); 1806: } 1807: } else { 1808: _error($logger, "$rule_description: Unknown rule '$rule_name'"); 1809: } 1810: } 1811: } elsif(ref($rules) eq 'ARRAY') { 1812: if(scalar(@{$rules})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1813: # An argument can be one of several different types. 1814: # This path handles both explicit array-of-rules schemas and the 1815: # normalised form of union type shorthand (type => ['a', 'b', ...]). 1816: my $rc = 0; 1817: my @types; 1818: foreach my $rule(@{$rules}) { 1819: if(ref($rule) ne 'HASH') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1820: _error($logger, "$rule_description: Parameter '$key' rules must be a hash reference"); 1821: next; 1822: } 1823: if(!defined($rule->{'type'})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1824: _error($logger, "$rule_description: Parameter '$key' is missing a type in an alternative"); 1825: next; 1826: } 1827: push @types, $rule->{'type'}; 1828: my $result; 1829: eval { 1830: $result = validate_strict({ input => { $key => $value }, schema => { $key => $rule }, logger => undef, custom_types => $custom_types }); 1831: }; 1832: if(!$@) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1833: # Capture coercion performed by the successful sub-validation 1834: # (e.g. integer/number coercion) so the outer scope sees it. 1835: $value = $result->{$key} if(defined($result)); 1836: $rc = 1; 1837: last; 1838: } 1839: } 1840: if(!$rc) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1841: _error($logger, "$rule_description: Parameter: '$key': must be one of " . join(', ', @types)); 1842: $invalid_args{$key} = 1; 1843: } 1844: } else { 1845: _error($logger, "$rule_description: Parameter: '$key': schema is empty arrayref"); 1846: } 1847: } elsif(ref($rules)) { 1848: _error($logger, 'rules must be a hash reference or string'); 1849: } 1850: 1851: $validated_args{$key} = $value; 1852: } 1853: 1854: # Validate parameter relationships 1855 → 1855 → 18591855 → 1855 → 0 1855: if (my $relationships = $params->{'relationships'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1856: _validate_relationships(\%validated_args, $relationships, $logger, $schema_description); 1857: } 1858: 1859 → 1859 → 18741859 → 1859 → 0 1859: if(my $cross_validation = $params->{'cross_validation'}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1860: foreach my $validator_name(keys %{$cross_validation}) { 1861: my $validator = $cross_validation->{$validator_name}; 1862: if((!ref($validator)) || (ref($validator) ne 'CODE')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1863: _error($logger, "$schema_description: cross_validation $validator is not a code snippet"); 1864: next; 1865: } 1866: if(my $error = &{$validator}(\%validated_args, $validator)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1867: _error($logger, $error); 1868: # We have no idea which parameters are still valid, so let's invalidate them all 1869: return; 1870: } 1871: } 1872: } 1873: 1874 → 1874 → 18781874 → 1874 → 0 1874: foreach my $key(keys %invalid_args) { 1875: delete $validated_args{$key}; 1876: } 1877: 1878 → 1878 → 18951878 → 1878 → 0 1878: if($are_positional_args == 1) {

Mutants (Total: 2, Killed: 2, Survived: 0)

1879: my @rc; 1880: foreach my $key (keys %{$schema}) { 1881: # Use exists() rather than if(my $value = ...) so that falsy but 1882: # valid coerced values (integer 0, empty string, undef from an 1883: # absent optional) are not silently dropped from the return array. 1884: if(exists $validated_args{$key}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1885: my $value = delete $validated_args{$key}; 1886: my $position = $schema->{$key}->{'position'}; 1887: if(defined($rc[$position])) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1888: _error($logger, "$schema_description: $key: position $position appears twice"); 1889: } 1890: $rc[$position] = $value; 1891: } 1892: } 1893: return \@rc; 1894: } 1895 → 1895 → 0 1895: return \%validated_args; 1896: } 1897: 1898: # _schema_from_arrayref($arrayref, $logger) 1899: # 1900: # Normalise an arrayref schema: 1901: # [ { name => 'param', type => 'string', ... }, ... ] 1902: # to the standard named-parameter hashref form: 1903: # { param => { type => 'string', ... }, ... } 1904: # 1905: # The 'name' key is consumed during conversion and does not become a rule. 1906: # Croaks if any element is not a hashref, is missing 'name', or if a name 1907: # appears more than once. 1908: sub _schema_from_arrayref 1909: { 1910 → 1913 → 19241910 → 1913 → 0 1910: my ($arrayref, $logger) = @_; 1911: 1912: my %schema; 1913: foreach my $spec (@{$arrayref}) { 1914: _error($logger, "validate_strict: each arrayref schema element must be a hashref") 1915: unless ref($spec) eq 'HASH'; 1916: _error($logger, "validate_strict: arrayref schema element must have a 'name' key") 1917: unless exists($spec->{'name'}); 1918: my %rule = %{$spec}; 1919: my $name = delete $rule{'name'}; 1920: _error($logger, "validate_strict: duplicate parameter '$name' in arrayref schema") 1921: if exists($schema{$name}); 1922: $schema{$name} = \%rule; 1923: } 1924 → 1924 → 0 1924: return \%schema; 1925: } 1926: 1927: # Return number of visible characters not number of bytes 1928: # Ensure string is decoded into Perl characters 1929: sub _number_of_characters 1930: { 1931 → 1935 → 19391931 → 1935 → 0 1931: my $value = $_[0]; 1932: 1933: return if(!defined($value)); 1934: 1935: if($value !~ /[^[:ascii:]]/) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1936: return length($value); 1937: } 1938: # Decode only if it's not already a Perl character string 1939 → 1945 → 0 1939: $value = decode_utf8($value) unless utf8::is_utf8($value); 1940: 1941: # Count grapheme clusters (visible characters) 1942: # The pseudo-operator () = forces list context to count matches 1943: # return scalar( () = $value =~ /\X/g ); 1944: 1945: return Unicode::GCString->new($value)->length();

Mutants (Total: 2, Killed: 2, Survived: 0)

1946: } 1947: 1948: sub _apply_nested_defaults { 1949 → 1952 → 19651949 → 1952 → 0 1949: my ($input, $schema) = @_; 1950: my %result = %$input; 1951: 1952: foreach my $key (keys %$schema) { 1953: my $rules = $schema->{$key}; 1954: 1955: if (ref $rules eq 'HASH' && exists $rules->{default} && !exists $result{$key}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1956: $result{$key} = $rules->{default}; 1957: } 1958: 1959: # Recursively handle nested schema 1960: if((ref $rules eq 'HASH') && $rules->{schema} && (ref $result{$key} eq 'HASH')) {

Mutants (Total: 1, Killed: 1, Survived: 0)

1961: $result{$key} = _apply_nested_defaults($result{$key}, $rules->{schema}); 1962: } 1963: } 1964: 1965 → 1965 → 0 1965: return \%result; 1966: } 1967: 1968: sub _validate_relationships { 1969 → 1973 → 0 1969: my ($validated_args, $relationships, $logger, $description) = @_; 1970: 1971: return unless ref($relationships) eq 'ARRAY'; 1972: 1973: foreach my $rel (@$relationships) { 1974: my $type = $rel->{type} or next; 1975: 1976: if ($type eq 'mutually_exclusive') {

Mutants (Total: 1, Killed: 1, Survived: 0)

1977: _validate_mutually_exclusive($validated_args, $rel, $logger, $description); 1978: } elsif ($type eq 'required_group') { 1979: _validate_required_group($validated_args, $rel, $logger, $description); 1980: } elsif ($type eq 'conditional_requirement') { 1981: _validate_conditional_requirement($validated_args, $rel, $logger, $description); 1982: } elsif ($type eq 'dependency') { 1983: _validate_dependency($validated_args, $rel, $logger, $description); 1984: } elsif ($type eq 'value_constraint') { 1985: _validate_value_constraint($validated_args, $rel, $logger, $description); 1986: } elsif ($type eq 'value_conditional') { 1987: _validate_value_conditional($validated_args, $rel, $logger, $description); 1988: } else { 1989: _error($logger, "Unknown relationship type $type"); 1990: } 1991: } 1992: } 1993: 1994: sub _validate_mutually_exclusive { 1995 → 2002 → 0 1995: my ($args, $rel, $logger, $description) = @_; 1996: 1997: my @params = @{$rel->{params} || []}; 1998: return unless @params >= 2;

Mutants (Total: 3, Killed: 3, Survived: 0)

1999: 2000: my @present = grep { exists($args->{$_}) && defined($args->{$_}) } @params; 2001: 2002: if (@present > 1) {

Mutants (Total: 4, Killed: 4, Survived: 0)

2003: my $msg = $rel->{description} || 'Cannot specify both ' . join(' and ', @present); 2004: _error($logger, "$description: $msg"); 2005: } 2006: } 2007: 2008: sub _validate_required_group { 2009 → 2016 → 0 2009: my ($args, $rel, $logger, $description) = @_; 2010: 2011: my @params = @{$rel->{params} || []}; 2012: return unless @params >= 2;

Mutants (Total: 3, Killed: 3, Survived: 0)

2013: 2014: my @present = grep { exists($args->{$_}) && defined($args->{$_}) } @params; 2015: 2016: if (@present == 0) {

Mutants (Total: 2, Killed: 2, Survived: 0)

2017: my $msg = $rel->{description} || 2018: 'Must specify at least one of: ' . join(', ', @params); 2019: _error($logger, "$description: $msg"); 2020: } 2021: } 2022: 2023: sub _validate_conditional_requirement { 2024 → 2030 → 0 2024: my ($args, $rel, $logger, $description) = @_; 2025: 2026: my $if_param = $rel->{if} or return; 2027: my $then_param = $rel->{then_required} or return; 2028: 2029: # If the condition parameter is present and defined 2030: if (exists($args->{$if_param}) && defined($args->{$if_param})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2031: # Check if it's truthy (for booleans and general values) 2032: if ($args->{$if_param}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2033: # Then the required parameter must also be present 2034: unless (exists($args->{$then_param}) && defined($args->{$then_param})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2035: my $msg = $rel->{description} || "When $if_param is specified, $then_param is required"; 2036: _error($logger, "$description: $msg"); 2037: } 2038: } 2039: } 2040: } 2041: 2042: sub _validate_dependency { 2043 → 2049 → 0 2043: my ($args, $rel, $logger, $description) = @_; 2044: 2045: my $param = $rel->{param} or return; 2046: my $requires = $rel->{requires} or return; 2047: 2048: # If param is present, requires must also be present 2049: if (exists($args->{$param}) && defined($args->{$param})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2050: unless (exists($args->{$requires}) && defined($args->{$requires})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2051: my $msg = $rel->{description} || "$param requires $requires to be specified"; 2052: _error($logger, "$description: $msg"); 2053: } 2054: } 2055: } 2056: 2057: sub _validate_value_constraint { 2058 → 2067 → 0 2058: my ($args, $rel, $logger, $description) = @_; 2059: 2060: my $if_param = $rel->{if} or return; 2061: my $then_param = $rel->{then} or return; 2062: my $operator = $rel->{operator} or return; 2063: my $value = $rel->{value}; 2064: return unless defined $value; 2065: 2066: # If the condition parameter is present and truthy 2067: if (exists($args->{$if_param}) && defined($args->{$if_param}) && $args->{$if_param}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2068: # Check if the then parameter exists 2069: if (exists($args->{$then_param}) && defined($args->{$then_param})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2070: my $actual = $args->{$then_param}; 2071: my $valid = 0; 2072: 2073: if ($operator eq '==') {

Mutants (Total: 1, Killed: 1, Survived: 0)

2074: $valid = ($actual == $value);

Mutants (Total: 1, Killed: 1, Survived: 0)

2075: } elsif ($operator eq '!=') { 2076: $valid = ($actual != $value);

Mutants (Total: 1, Killed: 1, Survived: 0)

2077: } elsif ($operator eq '<') { 2078: $valid = ($actual < $value);

Mutants (Total: 3, Killed: 3, Survived: 0)

2079: } elsif ($operator eq '<=') { 2080: $valid = ($actual <= $value);

Mutants (Total: 3, Killed: 3, Survived: 0)

2081: } elsif ($operator eq '>') { 2082: $valid = ($actual > $value);

Mutants (Total: 3, Killed: 3, Survived: 0)

2083: } elsif ($operator eq '>=') { 2084: $valid = ($actual >= $value);

Mutants (Total: 3, Killed: 3, Survived: 0)

2085: } 2086: 2087: unless ($valid) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2088: my $msg = $rel->{description} || "When $if_param is specified, $then_param must be $operator $value (got $actual)"; 2089: _error($logger, "$description: $msg"); 2090: } 2091: } 2092: } 2093: } 2094: 2095: sub _validate_value_conditional { 2096 → 2104 → 0 2096: my ($args, $rel, $logger, $description) = @_; 2097: 2098: my $if_param = $rel->{if} or return; 2099: my $equals = $rel->{equals}; 2100: my $then_param = $rel->{then_required} or return; 2101: return unless defined $equals; 2102: 2103: # If the parameter has the specific value 2104: if (exists($args->{$if_param}) && defined($args->{$if_param})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2105: if ($args->{$if_param} eq $equals) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2106: # Then the required parameter must be present 2107: unless (exists($args->{$then_param}) && defined($args->{$then_param})) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2108: my $msg = $rel->{description} || 2109: "When $if_param equals '$equals', $then_param is required"; 2110: _error($logger, "$description: $msg"); 2111: } 2112: } 2113: } 2114: } 2115: 2116: # Helper to log error or croak 2117: sub _error 2118: { 2119 → 2123 → 21262119 → 2123 → 0 2119: my $logger = shift; 2120: my $message = join('', @_); 2121: 2122: my @call_details = caller(0); 2123: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2124: $logger->error(__PACKAGE__, ' line ', $call_details[2], ": $message"); 2125: } 2126 → 2128 → 0 2126: croak(__PACKAGE__, ' line ', $call_details[2], ": $message"); 2127: # Be absolutely sure, sometimes croak doesn't die for me in Test::Most scripts 2128: die (__PACKAGE__, ' line ', $call_details[2], ": $message"); 2129: } 2130: 2131: # Helper to log warning or carp 2132: sub _warn 2133: { 2134 → 2137 → 0 2134: my $logger = shift; 2135: my $message = join('', @_); 2136: 2137: if($logger) {

Mutants (Total: 1, Killed: 1, Survived: 0)

2138: $logger->warn(__PACKAGE__, ": $message"); 2139: } else { 2140: carp(__PACKAGE__, ": $message"); 2141: } 2142: } 2143: 2144: =head1 AUTHOR 2145: 2146: Nigel Horne, C<< <njh at nigelhorne.com> >> 2147: 2148: =encoding utf-8 2149: 2150: =head1 FORMAL SPECIFICATION 2151: 2152: [PARAM_NAME, VALUE, TYPE_NAME, CONSTRAINT_VALUE] 2153: 2154: ValidationRule ::= SimpleType | ComplexRule | UnionType 2155: 2156: SimpleType ::= string | integer | number | arrayref | hashref | coderef | object 2157: 2158: UnionType ::= seq SimpleType -- at least two members; written as type => ['a', 'b'] 2159: 2160: ComplexRule == [ 2161: type: SimpleType | UnionType; 2162: min: ℕ₁; 2163: max: ℕ₁; 2164: optional: 𝔹; 2165: matches: REGEX; 2166: regex: REGEX; 2167: nomatch: REGEX; 2168: memberof: seq VALUE; 2169: enum: seq VALUE; 2170: values: seq VALUE; 2171: notmemberof: seq VALUE; 2172: callback: FUNCTION; 2173: isa: TYPE_NAME; 2174: can: METHOD_NAME 2175: ] 2176: 2177: Schema == PARAM_NAME ⇸ ValidationRule 2178: 2179: Arguments == PARAM_NAME ⇸ VALUE 2180: 2181: ValidatedResult == PARAM_NAME ⇸ VALUE 2182: 2183: ∀ rule: ComplexRule • 2184: rule.min ≤ rule.max ∧ 2185: ¬((rule.memberof ∨ rule.enum ∨ rule.values) ∧ rule.min) ∧ 2186: ¬((rule.memberof ∨ rule.enum ∨ rule.values) ∧ rule.max) ∧ 2187: ¬(rule.notmemberof ∧ rule.min) ∧ 2188: ¬(rule.notmemberof ∧ rule.max) 2189: 2190: ∀ schema: Schema; args: Arguments • 2191: dom(validate_strict(schema, args)) ⊆ dom(schema) ∪ dom(args) 2192: 2193: validate_strict: Schema × Arguments → ValidatedResult 2194: 2195: ∀ schema: Schema; args: Arguments • 2196: let result == validate_strict(schema, args) • 2197: (∀ name: dom(schema) ∩ dom(args) • 2198: name ∈ dom(result) ⇒ 2199: type_matches(result(name), schema(name))) ∧ 2200: (∀ name: dom(schema) • 2201: ¬optional(schema(name)) ⇒ name ∈ dom(args)) 2202: 2203: type_matches: VALUE × ValidationRule → 𝔹 2204: 2205: =head1 EXAMPLE 2206: 2207: use Params::Get; 2208: use Params::Validate::Strict; 2209: 2210: sub where_am_i 2211: { 2212: my $params = Params::Validate::Strict::validate_strict({ 2213: args => Params::Get::get_params(undef, \@_), 2214: description => 'Print a string of latitude and longitude', 2215: error_msg => 'Latitude is a number between +/- 90, longitude is a number between +/- 180', 2216: members => { 2217: 'latitude' => { 2218: type => 'number', 2219: min => -90, 2220: max => 90 2221: }, 'longitude' => { 2222: type => 'number', 2223: min => -180, 2224: max => 180 2225: } 2226: } 2227: }); 2228: 2229: print 'You are at ', $params->{'latitude'}, ', ', $params->{'longitude'}, "\n"; 2230: } 2231: 2232: where_am_i({ latitude => 3.14, longitude => -155 }); 2233: 2234: =head1 BUGS 2235: 2236: =head1 SEE ALSO 2237: 2238: =over 4 2239: 2240: =item * L<Test Dashboard|https://nigelhorne.github.io/Params-Validate-Strict/coverage/> 2241: 2242: =item * L<Data::Processor> 2243: 2244: =item * L<Params::Get> 2245: 2246: =item * L<Params::Smart> 2247: 2248: =item * L<Params::Validate> 2249: 2250: =item * L<Return::Set> 2251: 2252: =item * L<App::Test::Generator> 2253: 2254: =back 2255: 2256: =head1 SUPPORT 2257: 2258: This module is provided as-is without any warranty. 2259: 2260: Please report any bugs or feature requests to C<bug-params-validate-strict at rt.cpan.org>, 2261: or through the web interface at 2262: L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Params-Validate-Strict>. 2263: I will be notified, and then you'll 2264: automatically be notified of progress on your bug as I make changes. 2265: 2266: You can find documentation for this module with the perldoc command. 2267: 2268: perldoc Params::Validate::Strict 2269: 2270: You can also look for information at: 2271: 2272: =over 4 2273: 2274: =item * MetaCPAN 2275: 2276: L<https://metacpan.org/dist/Params-Validate-Strict> 2277: 2278: =item * RT: CPAN's request tracker 2279: 2280: L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Params-Validate-Strict> 2281: 2282: =item * CPAN Testers' Matrix 2283: 2284: L<http://matrix.cpantesters.org/?dist=Params-Validate-Strict> 2285: 2286: =item * CPAN Testers Dependencies 2287: 2288: L<http://deps.cpantesters.org/?module=Params::Validate::Strict> 2289: 2290: =back 2291: 2292: =head1 LICENSE AND COPYRIGHT 2293: 2294: Copyright 2025-2026 Nigel Horne. 2295: 2296: This program is released under the following licence: GPL2. 2297: If you use it, 2298: please let me know. 2299: 2300: =cut 2301: 2302: 1; 2303: 2304: __END__