lib/Params/Validate/Strict.pm

Structural Coverage (Approximate)

TER1 (Statement): 86.55%
TER2 (Branch): 86.67%
TER3 (LCSAJ): 100.0% (24/24)
Approximate LCSAJ segments: 631

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

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

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

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

1019: $unknown_parameter_handler = 'warn'; 1020: } else { 1021: $unknown_parameter_handler = 'die'; 1022: } 1023: } 1024: 1025 → 1029 → 1034 1025: return $args if(!defined($schema)); # No schema, allow all arguments

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

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

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

1030: $schema = _schema_from_arrayref($schema, $logger); 1031: } 1032: 1033: # Check if schema and args are references to hashes 1034 → 1034 → 1039 1034: if(ref($schema) ne 'HASH') {

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

1035: _error($logger, 'validate_strict: schema must be a hash reference'); 1036: } 1037: 1038: # Inspired by Data::Processor 1039 → 1042 → 1052 1039: my $schema_description = $params->{'description'} || 'validate_strict'; 1040: my $error_msg = $params->{'error_msg'}; 1041: 1042: if($schema->{'members'} && ($schema->{'description'} || $schema->{'error_msg'})) {

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

1043: $schema_description = $schema->{'description'}; 1044: $error_msg = $schema->{'error_msg'}; 1045: $schema = $schema->{'members'}; 1046: # The members value may also be in arrayref form 1047: if(ref($schema) eq 'ARRAY') {

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

1048: $schema = _schema_from_arrayref($schema, $logger); 1049: } 1050: } 1051: 1052 → 1052 → 1058 1052: if(exists($params->{'args'}) && (!defined($args))) {

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

1053: $args = {}; 1054: } elsif((ref($args) ne 'HASH') && (ref($args) ne 'ARRAY')) { 1055: _error($logger, $error_msg || "$schema_description: args must be a hash or array reference"); 1056: } 1057: 1058 → 1058 → 1080 1058: if(ref($args) eq 'HASH') {

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

1059: # Named args 1060: foreach my $key (keys %{$args}) { 1061: if(!exists($schema->{$key})) {

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

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

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

1063: _error($logger, "$schema_description: Unknown parameter '$key'"); 1064: } elsif($unknown_parameter_handler eq 'warn') { 1065: _warn($logger, "$schema_description: Unknown parameter '$key'"); 1066: next; 1067: } elsif($unknown_parameter_handler eq 'ignore') { 1068: if($logger) {

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

1069: $logger->debug(__PACKAGE__ . ": $schema_description: Unknown parameter '$key'"); 1070: } 1071: next; 1072: } else { 1073: _error($logger, "$schema_description: '$unknown_parameter_handler' unknown_parameter_handler must be one of die, warn, ignore"); 1074: } 1075: } 1076: } 1077: } 1078: 1079: # Find out if this routine takes positional arguments 1080 → 1081 → 1102 1080: my $are_positional_args = -1; 1081: foreach my $key (keys %{$schema}) { 1082: if(defined(my $rules = $schema->{$key})) {

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

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

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

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

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

1085: if($are_positional_args == 1) {

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

1086: _error($logger, "::validate_strict: $key is missing position value"); 1087: } 1088: $are_positional_args = 0; 1089: last; 1090: } 1091: $are_positional_args = 1; 1092: } else { 1093: $are_positional_args = 0; 1094: last; 1095: } 1096: } else { 1097: $are_positional_args = 0; 1098: last; 1099: } 1100: } 1101: 1102 → 1104 → 1911 1102: my %validated_args; 1103: my %invalid_args; 1104: foreach my $key (keys %{$schema}) { 1105: my $rules = $schema->{$key}; 1106: my $value; 1107: if($are_positional_args == 1) {

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

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

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

1109: _error($logger, "::validate_strict: position $rules->{position} given for '$key', but args isn't an array"); 1110: } 1111: $value = @{$args}[$rules->{'position'}]; 1112: } else { 1113: $value = $args->{$key}; 1114: } 1115: 1116: if(!defined($rules)) { # Allow anything

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

1117: $validated_args{$key} = $value; 1118: next; 1119: } 1120: 1121: # If rules are a simple type string 1122: if(ref($rules) eq '') {

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

1123: $rules = { type => $rules }; 1124: } 1125: 1126: my $is_optional = 0; 1127: 1128: my $rule_description = $schema_description; # Can be overridden in each element 1129: 1130: if(ref($rules) eq 'HASH') {

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

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

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

1132: $rule_description = $rules->{'description'}; 1133: } 1134: # For stringref: validate and dereference before transform so that 1135: # transform (and all subsequent rule handlers) see the plain string. 1136: # Preserve the original ref so optional => CODE receives what the caller passed. 1137: my $pre_deref_value = $value; 1138: my $is_stringref_type = defined($value) && defined($rules->{'type'}) && !ref($rules->{'type'}) && lc($rules->{'type'}) eq 'stringref'; 1139: if($is_stringref_type) {

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

1140: if(ref($value) ne 'SCALAR') {

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

1141: my $got = ref($value) ? 'a ' . ref($value) . ' reference' : 'a plain scalar'; 1142: _error($logger, $rules->{'error_msg'} || "$rule_description: Parameter '$key' must be a string reference, not $got"); 1143: } 1144: $value = ${$value}; 1145: } 1146: if($rules->{'transform'} && defined($value)) {

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

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

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

1148: $value = &{$rules->{'transform'}}($value); 1149: } else { 1150: _error($logger, "$rule_description: transforms must be a code ref"); 1151: } 1152: } 1153: if(exists($rules->{optional})) {

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

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

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

1155: # For stringref the coderef receives the original SCALAR ref (what 1156: # the caller supplied), not the internally-dereferenced plain string. 1157: # For all other types the post-transform value is passed, as before. 1158: my $opt_arg = $is_stringref_type ? $pre_deref_value : $value; 1159: $is_optional = &{$rules->{optional}}($opt_arg, $args); 1160: } else { 1161: $is_optional = $rules->{'optional'}; 1162: } 1163: } elsif($rules->{nullable}) { 1164: $is_optional = $rules->{'nullable'}; 1165: } 1166: } 1167: 1168: # Handle optional parameters 1169: if((ref($rules) eq 'HASH') && $is_optional) {

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

1170: my $look_for_default = 0; 1171: if($are_positional_args == 1) {

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

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

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

1174: $look_for_default = 1; 1175: } 1176: } else { 1177: if(!exists($args->{$key})) {

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

1178: $look_for_default = 1; 1179: } 1180: } 1181: if($look_for_default) {

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

1182: if($are_positional_args == 1) {

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

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

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

1184: # arg array is too short, so it must be missing 1185: _error($logger, "$rule_description: Required parameter '$key' is missing"); 1186: next; 1187: } 1188: } 1189: if(exists($rules->{'default'})) {

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

1190: # Populate missing optional parameters with the specified output values 1191: $validated_args{$key} = $rules->{'default'}; 1192: next; # default wins; do not fall through to the schema branch 1193: } 1194: 1195: if($rules->{'schema'}) {

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

1196: $value = _apply_nested_defaults({}, $rules->{'schema'}); 1197: next unless scalar(%{$value}); 1198: # The nested schema has a default value 1199: } else { 1200: next; # optional and missing 1201: } 1202: } 1203: } elsif((ref($args) eq 'HASH') && !exists($args->{$key})) { 1204: # The parameter is required 1205: # Use exists rather than defined, so that an undefined value can be passed, but the key is there 1206: _error($logger, "$rule_description: Required parameter '$key' is missing"); 1207: } 1208: 1209: # Normalise union type shorthand: { type => ['string', 'integer'], ... } 1210: # into the array-of-rules form that the ARRAY handler below already supports. 1211: # Each candidate type inherits all other constraints from the parent rule 1212: # (min, max, matches, optional, etc.) so they are each fully validated. 1213: # Must run after optional/transform handling above but before rule dispatch below. 1214: if(ref($rules) eq 'HASH' && ref($rules->{'type'}) eq 'ARRAY') {

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

1215: my %base = %{$rules}; 1216: my @type_list = @{delete $base{'type'}}; 1217: if(!@type_list) {

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

1218: _error($logger, "$rule_description: Parameter '$key': union type list must not be empty"); 1219: } 1220: # Expand into one full rule hash per candidate type 1221: $rules = [ map { { %base, type => $_ } } @type_list ]; 1222: } 1223: 1224: # Validate based on rules 1225: if(ref($rules) eq 'HASH') {

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

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

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

1227: if($min > $max) {

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

1228: _error($logger, "validate_strict($key): min must be <= max ($min > $max)"); 1229: } 1230: } 1231: 1232: # memberof and its synonym enum cannot be combined with min or max 1233: if($rules->{'memberof'} || $rules->{'enum'} || $rules->{'values'}) {

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

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

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

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

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

1238: _error($logger, "validate_strict($key): max ($max) makes no sense with memberof/enum/values"); 1239: } 1240: } 1241: 1242: foreach my $rule_name (keys %$rules) { 1243: my $rule_value = $rules->{$rule_name}; 1244: 1245: if((ref($rule_value) eq 'CODE')

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

1246: && ($rule_name ne 'validate') 1247: && ($rule_name ne 'callback') 1248: && ($rule_name ne 'validator') 1249: && ($rule_name ne 'transform') # already applied before this loop 1250: && ($rule_name ne 'optional')) { # already applied before this loop 1251: $rule_value = &{$rule_value}($value, $args); 1252: } 1253: 1254: # Better OOP, the routine has been given an object rather than a scalar 1255: if(Scalar::Util::blessed($rule_value) && $rule_value->can('as_string')) {

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

1256: $rule_value = $rule_value->as_string(); 1257: } 1258: 1259: if($rule_name eq 'type') {

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

1260: my $type = lc($rule_value); 1261: 1262: if(($type eq 'string') || ($type eq 'str')) {

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

1263: if(ref($value)) {

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

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

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

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

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

1271: next; # Skip if number is undefined 1272: } 1273: if(!Scalar::Util::looks_like_number($value) || ($value - $value) != 0 || $value != int($value)) {

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

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

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

1275: _error($logger, $rules->{'error_msg'}); 1276: } else { 1277: _error($logger, "$rule_description: Parameter '$key' ($value) must be an integer"); 1278: } 1279: } 1280: $value = int($value); # Coerce to integer 1281: } elsif(($type eq 'number') || ($type eq 'float') || ($type eq 'num') || ($type eq 'double')) { 1282: if(!defined($value)) {

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

1283: next; # Skip if number is undefined 1284: } 1285: if(!Scalar::Util::looks_like_number($value)) {

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

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

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

1287: _error($logger, $rules->{'error_msg'}); 1288: } else { 1289: _error($logger, "$rule_description: Parameter '$key' must be a number"); 1290: } 1291: } 1292: # $value = eval $value; # Coerce to number (be careful with eval) 1293: $value = 0 + $value; # Numeric coercion 1294: } elsif($type eq 'arrayref') { 1295: if(!defined($value)) {

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

1296: next; # Skip if arrayref is undefined 1297: } 1298: if(ref($value) ne 'ARRAY') {

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

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

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

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

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

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

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

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

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

1311: _error($logger, $rules->{'error_msg'}); 1312: } else { 1313: _error($logger, "$rule_description: Parameter '$key' must be an hashref"); 1314: } 1315: } 1316: } elsif($type eq 'scalar') { 1317: if(!defined($value)) {

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

1318: next; # Skip if undefined 1319: } 1320: if(ref($value)) {

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

1321: _error($logger, $rules->{'error_msg'} || "$rule_description: Parameter '$key' must be a scalar, not a " . ref($value) . ' reference'); 1322: } 1323: } elsif($type eq 'scalarref') { 1324: if(!defined($value)) {

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

1325: next; # Skip if undefined 1326: } 1327: if(ref($value) ne 'SCALAR') {

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

1328: my $got = ref($value) ? 'a ' . ref($value) . ' reference' : 'a plain scalar'; 1329: _error($logger, $rules->{'error_msg'} || "$rule_description: Parameter '$key' must be a scalar reference, not $got"); 1330: } 1331: } elsif($type eq 'stringref') { 1332: if(!defined($value)) {

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

1333: next; # Skip if undefined 1334: } 1335: # The early-deref block validated the SCALAR ref and set $value to the 1336: # plain string. If transform subsequently returned a reference, reject it. 1337: if(ref($value)) {

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

1338: _error($logger, $rules->{'error_msg'} || "$rule_description: Parameter '$key' stringref transform must return a plain string, not a " . ref($value) . ' reference'); 1339: } 1340: } elsif(($type eq 'boolean') || ($type eq 'bool')) { 1341: if(!defined($value)) {

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

1342: next; # Skip if bool is undefined 1343: } 1344: if(defined(my $b = $Readonly::Values::Boolean::booleans{$value})) {

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

1345: $value = $b; 1346: } else { 1347: if($rules->{'error_msg'}) {

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

1348: _error($logger, $rules->{'error_msg'}); 1349: } else { 1350: _error($logger, "$rule_description: Parameter '$key' ($value) must be a boolean"); 1351: } 1352: } 1353: } elsif($type eq 'coderef') { 1354: if(!defined($value)) {

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

1355: next; # Skip if code is undefined 1356: } 1357: if(ref($value) ne 'CODE') {

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

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

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

1359: _error($logger, $rules->{'error_msg'}); 1360: } else { 1361: _error($logger, "$rule_description: Parameter '$key' must be a coderef, not a ref to " . ref($value)); 1362: } 1363: } 1364: } elsif($type eq 'object') { 1365: if(!defined($value)) {

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

1366: next; # Skip if object is undefined 1367: } 1368: if(!Scalar::Util::blessed($value)) {

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

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

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

1370: _error($logger, $rules->{'error_msg'}); 1371: } else { 1372: _error($logger, "$rule_description: Parameter '$key' must be an object"); 1373: } 1374: } 1375: } elsif(my $custom_type = $custom_types->{$type}) { 1376: if($custom_type->{'transform'}) {

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

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

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

1379: $value = &{$custom_type->{'transform'}}($value); 1380: } else { 1381: _error($logger, "$rule_description: transforms must be a code ref"); 1382: next; 1383: } 1384: } 1385: validate_strict({ input => { $key => $value }, schema => { $key => $custom_type }, custom_types => $custom_types }); 1386: } else { 1387: _error($logger, "$rule_description: Unknown type '$type'"); 1388: } 1389: } elsif(($rule_name eq 'min') || ($rule_name eq 'minimum')) { 1390: if(!defined($rules->{'type'})) {

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

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

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

1395: $rule_value = $custom_types->{$type}->{'min'} // $custom_types->{$type}->{minumum}; 1396: $type = $custom_types->{$type}->{'type'}; 1397: } 1398: if(($type eq 'string') || ($type eq 'str') || ($type eq 'stringref')) {

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

1399: if($rule_value < 0) {

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

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

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

1401: _error($logger, $rules->{'error_msg'}); 1402: } else { 1403: _error($logger, "$rule_description: String parameter '$key' has meaningless minimum value that is less than zero"); 1404: } 1405: } 1406: if(!defined($value)) {

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

1407: next; # Skip if string is undefined 1408: } 1409: if(defined(my $len = _number_of_characters($value))) {

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

1410: if($len < $rule_value) {

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

1411: _error($logger, $rules->{'error_msg'} || "$rule_description: String parameter '$key' too short, ($len characters), must be at least $rule_value characters"); 1412: $invalid_args{$key} = 1; 1413: } 1414: } else { 1415: _error($logger, $rules->{'error_msg'} || "$rule_description: '$key' can't be decoded"); 1416: $invalid_args{$key} = 1; 1417: } 1418: } elsif($type eq 'arrayref') { 1419: if(!defined($value)) {

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

1420: next; # Skip if array is undefined 1421: } 1422: if(ref($value) ne 'ARRAY') {

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

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

Mutants (Total: 1, Killed: 0, Survived: 1)
1424: _error($logger, $rules->{'error_msg'}); 1425: } else { 1426: _error($logger, "$rule_description: Parameter '$key' must be an arrayref, not " . ref($value)); 1427: } 1428: } 1429: if(scalar(@{$value}) < $rule_value) {

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

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

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

1431: _error($logger, $rules->{'error_msg'}); 1432: } else { 1433: _error($logger, "$rule_description: Parameter '$key' must be at least length $rule_value"); 1434: } 1435: $invalid_args{$key} = 1; 1436: } 1437: } elsif($type eq 'hashref') { 1438: if(!defined($value)) {

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

1439: next; # Skip if hash is undefined 1440: } 1441: if(scalar(keys(%{$value})) < $rule_value) {

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

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

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

1443: _error($logger, $rules->{'error_msg'}); 1444: } else { 1445: _error($logger, "$rule_description: Parameter '$key' must contain at least $rule_value keys"); 1446: } 1447: $invalid_args{$key} = 1; 1448: } 1449: } elsif(($type eq 'integer') || ($type eq 'number') || ($type eq 'float')) { 1450: if(!defined($value)) {

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

1451: next; # Skip if hash is undefined 1452: } 1453: if(Scalar::Util::looks_like_number($value)) {

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

1454: if($value < $rule_value) {

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

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

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

1456: _error($logger, $rules->{'error_msg'}); 1457: } elsif(($type eq 'integer') && ($value == 0)) {

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

1458: _error($logger, "$rule_description: Parameter '$key' ($value) must be a positive number"); 1459: } elsif(($type eq 'integer') && ($value == 1)) {

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

1460: _error($logger, "$rule_description: Parameter '$key' ($value) must be a positive, non-zero number"); 1461: } else { 1462: _error($logger, "$rule_description: Parameter '$key' ($value) must be at least $rule_value"); 1463: } 1464: $invalid_args{$key} = 1; 1465: next; 1466: } 1467: } else { 1468: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1469: _error($logger, $rules->{'error_msg'}); 1470: } else { 1471: _error($logger, "$rule_description: Parameter '$key' ($value) must be a number"); 1472: } 1473: next; 1474: } 1475: } else { 1476: _error($logger, "$rule_description: Parameter '$key' of type '$type' has meaningless min value $rule_value"); 1477: } 1478: } elsif($rule_name eq 'max') { 1479: if(!defined($rules->{'type'})) {

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

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

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

1484: $rule_value = $custom_types->{$type}->{'max'}; 1485: $type = $custom_types->{$type}->{'type'}; 1486: } 1487: if(($type eq 'string') || ($type eq 'str') || ($type eq 'stringref')) {

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

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

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

1489: next; # Skip if string is undefined 1490: } 1491: if(defined(my $len = _number_of_characters($value))) {

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

1492: if($len > $rule_value) {

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

1493: _error($logger, $rules->{'error_msg'} || "$rule_description: String parameter '$key' too long, ($len characters), must be no longer than $rule_value"); 1494: $invalid_args{$key} = 1; 1495: } 1496: } else { 1497: _error($logger, $rules->{'error_msg'} || "$rule_description: '$key' can't be decoded"); 1498: $invalid_args{$key} = 1; 1499: } 1500: } elsif($type eq 'arrayref') { 1501: if(!defined($value)) {

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

1502: next; # Skip if string is undefined 1503: } 1504: if(ref($value) ne 'ARRAY') {

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

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

Mutants (Total: 1, Killed: 0, Survived: 1)
1506: _error($logger, $rules->{'error_msg'}); 1507: } else { 1508: _error($logger, "$rule_description: Parameter '$key' must be an arrayref, not " . ref($value)); 1509: } 1510: } 1511: if(scalar(@{$value}) > $rule_value) {

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

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

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

1513: _error($logger, $rules->{'error_msg'}); 1514: } else { 1515: _error($logger, "$rule_description: Parameter '$key' must contain no more than $rule_value items"); 1516: } 1517: $invalid_args{$key} = 1; 1518: } 1519: } elsif($type eq 'hashref') { 1520: if(!defined($value)) {

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

1521: next; # Skip if hash is undefined 1522: } 1523: if(scalar(keys(%{$value})) > $rule_value) {

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

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

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

1525: _error($logger, $rules->{'error_msg'}); 1526: } else { 1527: _error($logger, "$rule_description: Parameter '$key' must contain no more than $rule_value keys"); 1528: } 1529: $invalid_args{$key} = 1; 1530: } 1531: } elsif(($type eq 'integer') || ($type eq 'number') || ($type eq 'float')) { 1532: if(!defined($value)) {

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

1533: next; # Skip if hash is undefined 1534: } 1535: if(Scalar::Util::looks_like_number($value)) {

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

1536: if($value > $rule_value) {

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

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

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

1538: _error($logger, $rules->{'error_msg'}); 1539: } elsif(($type eq 'integer') && ($value == 0)) {

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

1540: _error($logger, "$rule_description: Parameter '$key' ($value) must be a negative number"); 1541: } elsif(($type eq 'integer') && ($value == -1)) {

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

1542: _error($logger, "$rule_description: Parameter '$key' ($value) must be a negative, non-zero number"); 1543: } else { 1544: _error($logger, "$rule_description: Parameter '$key' ($value) must be no more than $rule_value"); 1545: } 1546: $invalid_args{$key} = 1; 1547: next; 1548: } 1549: } else { 1550: if($rules->{'error_msg'}) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1551: _error($logger, $rules->{'error_msg'}); 1552: } else { 1553: _error($logger, "$rule_description: Parameter '$key' ($value) must be a number"); 1554: } 1555: next; 1556: } 1557: } else { 1558: _error($logger, "$rule_description: Parameter '$key' of type '$type' has meaningless max value $rule_value"); 1559: } 1560: } elsif(($rule_name eq 'matches') || ($rule_name eq 'regex')) { 1561: if(!defined($value)) {

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

1562: next; # Skip if string is undefined 1563: } 1564: eval { 1565: my $re = (ref($rule_value) eq 'Regexp') ? $rule_value : qr/\Q$rule_value\E/; 1566: if(($rules->{'type'} eq 'arrayref') || ($rules->{'type'} eq 'ArrayRef')) {

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

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

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

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

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

1570: _error($logger, $rules->{'error_msg'}); 1571: } else { 1572: _error($logger, "$rule_description: All members of parameter '$key' [", join(', ', @{$value}), "] must match pattern '$rule_value'"); 1573: } 1574: } 1575: } elsif($value !~ $re) { 1576: if($rules->{'error_msg'}) {

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

1577: _error($logger, $rules->{'error_msg'}); 1578: } else { 1579: _error($logger, "$rule_description: Parameter '$key' ($value) must match pattern '$re'"); 1580: } 1581: } 1582: 1; 1583: }; 1584: if($@) {

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

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

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

1586: _error($logger, $rules->{'error_msg'}); 1587: } else { 1588: _error($logger, "$rule_description: Parameter '$key' regex '$rule_value' error: $@"); 1589: } 1590: $invalid_args{$key} = 1; 1591: } 1592: } elsif($rule_name eq 'nomatch') { 1593: if(!defined($value)) {

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

1594: next; # Skip if string is undefined 1595: } 1596: if(($rules->{'type'} eq 'arrayref') || ($rules->{'type'} eq 'ArrayRef')) {

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

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

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

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

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

1600: _error($logger, $rules->{'error_msg'}); 1601: } else { 1602: _error($logger, "$rule_description: No member of parameter '$key' [", join(', ', @{$value}), "] must match pattern '$rule_value'"); 1603: } 1604: } 1605: } elsif($value =~ $rule_value) { 1606: if($rules->{'error_msg'}) {

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

1607: _error($logger, $rules->{'error_msg'}); 1608: } else { 1609: _error($logger, "$rule_description: Parameter '$key' ($value) must not match pattern '$rule_value'"); 1610: } 1611: $invalid_args{$key} = 1; 1612: } 1613: } elsif(($rule_name eq 'memberof') || ($rule_name eq 'enum') || ($rule_name eq 'values')) { 1614: if(!defined($value)) {

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

1615: next; # Skip if string is undefined 1616: } 1617: if(ref($rule_value) eq 'ARRAY') {

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

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

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

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

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

1621: $ok = 0; 1622: } 1623: } else { 1624: my $l = lc($value); 1625: unless(List::Util::any { (!defined($rules->{'case_sensitive'}) || ($rules->{'case_sensitive'} == 1)) ? $_ eq $value : lc($_) eq $l } @{$rule_value}) {

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

1626: $ok = 0; 1627: } 1628: } 1629: 1630: if(!$ok) {

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

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

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

1632: _error($logger, $rules->{'error_msg'}); 1633: } else { 1634: _error($logger, "$rule_description: Parameter '$key' ($value) must be one of ", join(', ', @{$rule_value})); 1635: } 1636: $invalid_args{$key} = 1; 1637: } 1638: } else { 1639: if($rules->{'error_msg'}) {

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

1640: _error($logger, $rules->{'error_msg'}); 1641: } else { 1642: _error($logger, "$rule_description: Parameter '$key' rule ($rule_value) must be an array reference"); 1643: } 1644: } 1645: } elsif($rule_name eq 'notmemberof') { 1646: if(!defined($value)) {

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

1647: next; # Skip if string is undefined 1648: } 1649: if(ref($rule_value) eq 'ARRAY') {

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

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

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

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

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

1653: $ok = 0; 1654: } 1655: } else { 1656: my $l = lc($value); 1657: if(List::Util::any { (!defined($rules->{'case_sensitive'}) || ($rules->{'case_sensitive'} == 1)) ? $_ eq $value : lc($_) eq $l } @{$rule_value}) {

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

1658: $ok = 0; 1659: } 1660: } 1661: 1662: if(!$ok) {

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

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

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

1664: _error($logger, $rules->{'error_msg'}); 1665: } else { 1666: _error($logger, "$rule_description: Parameter '$key' ($value) must not be one of ", join(', ', @{$rule_value})); 1667: } 1668: $invalid_args{$key} = 1; 1669: } 1670: } else { 1671: if($rules->{'error_msg'}) {

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

1672: _error($logger, $rules->{'error_msg'}); 1673: } else { 1674: _error($logger, "$rule_description: Parameter '$key' rule ($rule_value) must be an array reference"); 1675: } 1676: } 1677: } elsif($rule_name eq 'isa') { 1678: if(!defined($value)) {

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

1679: next; # Skip if object not given 1680: } 1681: if($rules->{'type'} eq 'object') {

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

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

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

1683: _error($logger, "$rule_description: Parameter '$key' must be a '$rule_value' object got a " . (ref($value) ? ref($value) : $value) . ' object instead'); 1684: $invalid_args{$key} = 1; 1685: } 1686: } else { 1687: _error($logger, "$rule_description: Parameter '$key' has meaningless isa value $rule_value"); 1688: } 1689: } elsif($rule_name eq 'can') { 1690: if(!defined($value)) {

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

1691: next; # Skip if object not given 1692: } 1693: if($rules->{'type'} eq 'object') {

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

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

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

1695: # List of methods 1696: foreach my $method(@{$rule_value}) { 1697: if(!$value->can($method)) {

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

1698: _error($logger, "$rule_description: Parameter '$key' must be an object that understands the $method method"); 1699: $invalid_args{$key} = 1; 1700: } 1701: } 1702: } elsif(!ref($rule_value)) { 1703: if(!$value->can($rule_value)) {

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

1704: _error($logger, "$rule_description: Parameter '$key' must be an object that understands the $rule_value method"); 1705: $invalid_args{$key} = 1; 1706: } 1707: } else { 1708: _error($logger, "$rule_description: 'can' rule for Parameter '$key must be either a scalar or an arrayref"); 1709: } 1710: } else { 1711: _error($logger, "$rule_description: Parameter '$key' has meaningless can value '$rule_value' for parameter type $rules->{type}"); 1712: } 1713: } elsif($rule_name eq 'element_type') { 1714: if(($rules->{'type'} eq 'arrayref') || ($rules->{'type'} eq 'ArrayRef')) {

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

1715: my $type = $rule_value; 1716: my $custom_type = $custom_types->{$rule_value}; 1717: if($custom_type && $custom_type->{'type'}) {

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

1718: $type = $custom_type->{'type'}; 1719: } 1720: foreach my $member(@{$value}) { 1721: if($custom_type && $custom_type->{'transform'}) {

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

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

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

1724: $member = &{$custom_type->{'transform'}}($member); 1725: } else { 1726: _error($logger, "$rule_description: transforms must be a code ref"); 1727: last; 1728: } 1729: } 1730: if(($type eq 'string') || ($type eq 'Str')) {

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

1731: if(ref($member)) {

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

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

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

1733: _error($logger, $rules->{'error_msg'}); 1734: } else { 1735: _error($logger, "$key can only contain strings"); 1736: } 1737: $invalid_args{$key} = 1; 1738: } 1739: } elsif($type eq 'integer') { 1740: if(ref($member) || ($member =~ /\D/)) {

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

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

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

1742: _error($logger, $rules->{'error_msg'}); 1743: } else { 1744: _error($logger, "$key can only contain integers (found $member)"); 1745: } 1746: $invalid_args{$key} = 1; 1747: } 1748: } elsif(($type eq 'number') || ($rule_value eq 'float')) { 1749: if(ref($member) || ($member !~ /^[-+]?(\d*\.\d+|\d+\.?\d*)$/)) {

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

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

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

1751: _error($logger, $rules->{'error_msg'}); 1752: } else { 1753: _error($logger, "$key can only contain numbers (found $member)"); 1754: } 1755: $invalid_args{$key} = 1; 1756: } 1757: } else { 1758: _error($logger, "BUG: Add $type to element_type list"); 1759: } 1760: } 1761: } else { 1762: _error($logger, "$rule_description: Parameter '$key' has meaningless element_type value $rule_value"); 1763: } 1764: } elsif($rule_name eq 'optional') { 1765: # Already handled at the beginning of the loop 1766: } elsif($rule_name eq 'nullable') { 1767: # Already handled at the beginning of the loop (same as optional) 1768: } elsif($rule_name eq 'default') { 1769: # Handled earlier 1770: } elsif($rule_name eq 'error_msg') { 1771: # Handled inline 1772: } elsif($rule_name eq 'transform') { 1773: # Handled before the loop 1774: } elsif($rule_name eq 'case_sensitive') { 1775: # Handled inline 1776: } elsif($rule_name eq 'description') { 1777: # A la, Data::Processor 1778: } elsif($rule_name =~ /^_/) { 1779: # Ignore internal/metadata fields from schema extraction 1780: } elsif($rule_name eq 'semantic') { 1781: if($rule_value eq 'unix_timestamp') {

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

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

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

1783: error($logger, 'Invalid Unix timestamp: $value'); 1784: } 1785: } else { 1786: _warn($logger, "semantic type $rule_value is not yet supported"); 1787: } 1788: } elsif($rule_name eq 'schema') { 1789: # Nested schema Run the given schema against each element of the array 1790: if(($rules->{'type'} eq 'arrayref') || ($rules->{'type'} eq 'ArrayRef')) {

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

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

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

1792: foreach my $member(@{$value}) { 1793: # Distinguish two schema forms: 1794: # (a) Rule hash — has a top-level 'type' key, e.g. { type=>'string', matches=>qr/.../ } 1795: # → validate each element against that rule directly. 1796: # (b) Field-schema hash — keys are field names whose values are rule hashes, 1797: # e.g. { name=>{type=>'string'}, age=>{type=>'integer'} } 1798: # → validate each hashref element against the field schema directly. 1799: my $is_field_schema = (ref($rule_value) eq 'HASH') && !exists($rule_value->{'type'}); 1800: my %inner = (custom_types => $custom_types); 1801: if($is_field_schema) {

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

1802: $inner{input} = $member; 1803: $inner{schema} = $rule_value; 1804: } else { 1805: $inner{input} = { $key => $member }; 1806: $inner{schema} = { $key => $rule_value }; 1807: } 1808: if(!validate_strict(\%inner)) {

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

1809: $invalid_args{$key} = 1; 1810: } 1811: } 1812: } elsif(defined($value)) { # Allow undef for optional values 1813: _error($logger, "$rule_description: nested schema: Parameter '$value' must be an arrayref"); 1814: } 1815: } elsif($rules->{'type'} eq 'hashref') { 1816: if(ref($rule_value) eq 'HASH') {

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

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

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

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

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

1821: $value = $new_args; 1822: } else { 1823: $invalid_args{$key} = 1; 1824: } 1825: } 1826: } else { 1827: _error($logger, "$rule_description: nested schema: Parameter '$value' must be an hashref"); 1828: } 1829: } else { 1830: _error($logger, "$rule_description: Parameter '$key': 'schema' only supports arrayref and hashref, not $rules->{type}"); 1831: } 1832: } elsif(($rule_name eq 'validate') || ($rule_name eq 'validator')) { 1833: if(ref($rule_value) eq 'CODE') {

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

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

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

1835: _error($logger, "$rule_description: $key not valid: $error"); 1836: $invalid_args{$key} = 1; 1837: } 1838: } else { 1839: # _error($logger, "$rule_description: Parameter '$key': 'validate' only supports coderef, not $value"); 1840: _error($logger, "$rule_description: Parameter '$key': 'validate' only supports coderef, not " . ref($rule_value) // $rule_value); 1841: } 1842: } elsif ($rule_name eq 'callback') { 1843: # Custom validation code 1844: unless (defined &$rule_value) {

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

1845: _error($logger, "$rule_description: callback for '$key' must be a code reference"); 1846: } 1847: my $res = $rule_value->($value, $args, $schema); 1848: unless ($res) {

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

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

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

1850: _error($logger, $rules->{'error_msg'}); 1851: } else { 1852: _error($logger, "$rule_description: Parameter '$key' failed custom validation"); 1853: } 1854: $invalid_args{$key} = 1; 1855: } 1856: } elsif($rule_name eq 'position') { 1857: if($rule_value =~ /\D/) {

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

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

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

1861: _error($logger, "$rule_description: Parameter '$key': 'position' must be a positive integer, not $value"); 1862: } 1863: } else { 1864: _error($logger, "$rule_description: Unknown rule '$rule_name'"); 1865: } 1866: } 1867: } elsif(ref($rules) eq 'ARRAY') { 1868: if(scalar(@{$rules})) {

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

1869: # An argument can be one of several different types. 1870: # This path handles both explicit array-of-rules schemas and the 1871: # normalised form of union type shorthand (type => ['a', 'b', ...]). 1872: my $rc = 0; 1873: my @types; 1874: foreach my $rule(@{$rules}) { 1875: if(ref($rule) ne 'HASH') {

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

1876: _error($logger, "$rule_description: Parameter '$key' rules must be a hash reference"); 1877: next; 1878: } 1879: if(!defined($rule->{'type'})) {

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

1880: _error($logger, "$rule_description: Parameter '$key' is missing a type in an alternative"); 1881: next; 1882: } 1883: push @types, $rule->{'type'}; 1884: my $result; 1885: eval { 1886: $result = validate_strict({ input => { $key => $value }, schema => { $key => $rule }, logger => undef, custom_types => $custom_types }); 1887: }; 1888: if(!$@) {

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

1889: # Capture coercion performed by the successful sub-validation 1890: # (e.g. integer/number coercion) so the outer scope sees it. 1891: $value = $result->{$key} if(defined($result)); 1892: $rc = 1; 1893: last; 1894: } 1895: } 1896: if(!$rc) {

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

1897: _error($logger, "$rule_description: Parameter: '$key': must be one of " . join(', ', @types)); 1898: $invalid_args{$key} = 1; 1899: } 1900: } else { 1901: _error($logger, "$rule_description: Parameter: '$key': schema is empty arrayref"); 1902: } 1903: } elsif(ref($rules)) { 1904: _error($logger, 'rules must be a hash reference or string'); 1905: } 1906: 1907: $validated_args{$key} = $value; 1908: } 1909: 1910: # Validate parameter relationships 1911 → 1911 → 1915 1911: if (my $relationships = $params->{'relationships'}) {

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

1912: _validate_relationships(\%validated_args, $relationships, $logger, $schema_description); 1913: } 1914: 1915 → 1915 → 1930 1915: if(my $cross_validation = $params->{'cross_validation'}) {

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

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

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

1919: _error($logger, "$schema_description: cross_validation $validator is not a code snippet"); 1920: next; 1921: } 1922: if(my $error = &{$validator}(\%validated_args, $validator)) {

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

1923: _error($logger, $error); 1924: # We have no idea which parameters are still valid, so let's invalidate them all 1925: return; 1926: } 1927: } 1928: } 1929: 1930 → 1930 → 1934 1930: foreach my $key(keys %invalid_args) { 1931: delete $validated_args{$key}; 1932: } 1933: 1934 → 1934 → 1951 1934: if($are_positional_args == 1) {

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

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

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

1941: my $value = delete $validated_args{$key}; 1942: my $position = $schema->{$key}->{'position'}; 1943: if(defined($rc[$position])) {

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

1944: _error($logger, "$schema_description: $key: position $position appears twice"); 1945: } 1946: $rc[$position] = $value; 1947: } 1948: } 1949: return \@rc;

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

1950: } 1951: return \%validated_args;

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

1952: } 1953: 1954: # _schema_from_arrayref($arrayref, $logger) 1955: # 1956: # Normalise an arrayref schema: 1957: # [ { name => 'param', type => 'string', ... }, ... ] 1958: # to the standard named-parameter hashref form: 1959: # { param => { type => 'string', ... }, ... } 1960: # 1961: # The 'name' key is consumed during conversion and does not become a rule. 1962: # Croaks if any element is not a hashref, is missing 'name', or if a name 1963: # appears more than once. 1964: sub _schema_from_arrayref 1965: { 1966 → 1969 → 1980 1966: my ($arrayref, $logger) = @_; 1967: 1968: my %schema; 1969: foreach my $spec (@{$arrayref}) { 1970: _error($logger, "validate_strict: each arrayref schema element must be a hashref") 1971: unless ref($spec) eq 'HASH'; 1972: _error($logger, "validate_strict: arrayref schema element must have a 'name' key") 1973: unless exists($spec->{'name'}); 1974: my %rule = %{$spec}; 1975: my $name = delete $rule{'name'}; 1976: _error($logger, "validate_strict: duplicate parameter '$name' in arrayref schema") 1977: if exists($schema{$name}); 1978: $schema{$name} = \%rule; 1979: } 1980: return \%schema;

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

1981: } 1982: 1983: # Return number of visible characters not number of bytes 1984: # Ensure string is decoded into Perl characters 1985: sub _number_of_characters 1986: { 1987 → 1991 → 1995 1987: my $value = $_[0]; 1988: 1989: return if(!defined($value)); 1990: 1991: if($value !~ /[^[:ascii:]]/) {

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

1992: return length($value);

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

1993: } 1994: # Decode only if it's not already a Perl character string 1995: $value = decode_utf8($value) unless utf8::is_utf8($value); 1996: 1997: # Count grapheme clusters (visible characters) 1998: # The pseudo-operator () = forces list context to count matches 1999: # return scalar( () = $value =~ /\X/g ); 2000: 2001: return Unicode::GCString->new($value)->length();

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

2002: } 2003: 2004: sub _apply_nested_defaults { 2005 → 2008 → 2021 2005: my ($input, $schema) = @_; 2006: my %result = %$input; 2007: 2008: foreach my $key (keys %$schema) { 2009: my $rules = $schema->{$key}; 2010: 2011: if (ref $rules eq 'HASH' && exists $rules->{default} && !exists $result{$key}) {

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

2012: $result{$key} = $rules->{default}; 2013: } 2014: 2015: # Recursively handle nested schema 2016: if((ref $rules eq 'HASH') && $rules->{schema} && (ref $result{$key} eq 'HASH')) {

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

2017: $result{$key} = _apply_nested_defaults($result{$key}, $rules->{schema}); 2018: } 2019: } 2020: 2021: return \%result;

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

2022: } 2023: 2024: sub _validate_relationships { 2025 → 2029 → 0 2025: my ($validated_args, $relationships, $logger, $description) = @_; 2026: 2027: return unless ref($relationships) eq 'ARRAY'; 2028: 2029: foreach my $rel (@$relationships) { 2030: my $type = $rel->{type} or next; 2031: 2032: if ($type eq 'mutually_exclusive') {

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

2033: _validate_mutually_exclusive($validated_args, $rel, $logger, $description); 2034: } elsif ($type eq 'required_group') { 2035: _validate_required_group($validated_args, $rel, $logger, $description); 2036: } elsif ($type eq 'conditional_requirement') { 2037: _validate_conditional_requirement($validated_args, $rel, $logger, $description); 2038: } elsif ($type eq 'dependency') { 2039: _validate_dependency($validated_args, $rel, $logger, $description); 2040: } elsif ($type eq 'value_constraint') { 2041: _validate_value_constraint($validated_args, $rel, $logger, $description); 2042: } elsif ($type eq 'value_conditional') { 2043: _validate_value_conditional($validated_args, $rel, $logger, $description); 2044: } else { 2045: _error($logger, "Unknown relationship type $type"); 2046: } 2047: } 2048: } 2049: 2050: sub _validate_mutually_exclusive { 2051 → 2058 → 0 2051: my ($args, $rel, $logger, $description) = @_; 2052: 2053: my @params = @{$rel->{params} || []}; 2054: return unless @params >= 2;

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

2055: 2056: my @present = grep { exists($args->{$_}) && defined($args->{$_}) } @params; 2057: 2058: if (@present > 1) {

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

2059: my $msg = $rel->{description} || 'Cannot specify both ' . join(' and ', @present); 2060: _error($logger, "$description: $msg"); 2061: } 2062: } 2063: 2064: sub _validate_required_group { 2065 → 2072 → 0 2065: my ($args, $rel, $logger, $description) = @_; 2066: 2067: my @params = @{$rel->{params} || []}; 2068: return unless @params >= 2;

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

2069: 2070: my @present = grep { exists($args->{$_}) && defined($args->{$_}) } @params; 2071: 2072: if (@present == 0) {

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

2073: my $msg = $rel->{description} || 2074: 'Must specify at least one of: ' . join(', ', @params); 2075: _error($logger, "$description: $msg"); 2076: } 2077: } 2078: 2079: sub _validate_conditional_requirement { 2080 → 2086 → 0 2080: my ($args, $rel, $logger, $description) = @_; 2081: 2082: my $if_param = $rel->{if} or return; 2083: my $then_param = $rel->{then_required} or return; 2084: 2085: # If the condition parameter is present and defined 2086: if (exists($args->{$if_param}) && defined($args->{$if_param})) {

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

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

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

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

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

2091: my $msg = $rel->{description} || "When $if_param is specified, $then_param is required"; 2092: _error($logger, "$description: $msg"); 2093: } 2094: } 2095: } 2096: } 2097: 2098: sub _validate_dependency { 2099 → 2105 → 0 2099: my ($args, $rel, $logger, $description) = @_; 2100: 2101: my $param = $rel->{param} or return; 2102: my $requires = $rel->{requires} or return; 2103: 2104: # If param is present, requires must also be present 2105: if (exists($args->{$param}) && defined($args->{$param})) {

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

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

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

2107: my $msg = $rel->{description} || "$param requires $requires to be specified"; 2108: _error($logger, "$description: $msg"); 2109: } 2110: } 2111: } 2112: 2113: sub _validate_value_constraint { 2114 → 2123 → 0 2114: my ($args, $rel, $logger, $description) = @_; 2115: 2116: my $if_param = $rel->{if} or return; 2117: my $then_param = $rel->{then} or return; 2118: my $operator = $rel->{operator} or return; 2119: my $value = $rel->{value}; 2120: return unless defined $value; 2121: 2122: # If the condition parameter is present and truthy 2123: if (exists($args->{$if_param}) && defined($args->{$if_param}) && $args->{$if_param}) {

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

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

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

2126: my $actual = $args->{$then_param}; 2127: my $valid = 0; 2128: 2129: if ($operator eq '==') {

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

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

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

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

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

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

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

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

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

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

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

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

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

2141: } 2142: 2143: unless ($valid) {

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

2144: my $msg = $rel->{description} || "When $if_param is specified, $then_param must be $operator $value (got $actual)"; 2145: _error($logger, "$description: $msg"); 2146: } 2147: } 2148: } 2149: } 2150: 2151: sub _validate_value_conditional { 2152 → 2160 → 0 2152: my ($args, $rel, $logger, $description) = @_; 2153: 2154: my $if_param = $rel->{if} or return; 2155: my $equals = $rel->{equals}; 2156: my $then_param = $rel->{then_required} or return; 2157: return unless defined $equals; 2158: 2159: # If the parameter has the specific value 2160: if (exists($args->{$if_param}) && defined($args->{$if_param})) {

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

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

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

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

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

2164: my $msg = $rel->{description} || 2165: "When $if_param equals '$equals', $then_param is required"; 2166: _error($logger, "$description: $msg"); 2167: } 2168: } 2169: } 2170: } 2171: 2172: # Helper to log error or croak 2173: sub _error 2174: { 2175 → 2179 → 2182 2175: my $logger = shift; 2176: my $message = join('', @_); 2177: 2178: my @call_details = caller(0); 2179: if($logger) {

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

2180: $logger->error(__PACKAGE__, ' line ', $call_details[2], ": $message"); 2181: } 2182: croak(__PACKAGE__, ' line ', $call_details[2], ": $message"); 2183: # Be absolutely sure, sometimes croak doesn't die for me in Test::Most scripts 2184: die (__PACKAGE__, ' line ', $call_details[2], ": $message"); 2185: } 2186: 2187: # Helper to log warning or carp 2188: sub _warn 2189: { 2190 → 2193 → 0 2190: my $logger = shift; 2191: my $message = join('', @_); 2192: 2193: if($logger) {

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

2194: $logger->warn(__PACKAGE__, ": $message"); 2195: } else { 2196: carp(__PACKAGE__, ": $message"); 2197: } 2198: } 2199: 2200: =head1 AUTHOR 2201: 2202: Nigel Horne, C<< <njh at nigelhorne.com> >> 2203: 2204: =encoding utf-8 2205: 2206: =head1 FORMAL SPECIFICATION 2207: 2208: [PARAM_NAME, VALUE, TYPE_NAME, CONSTRAINT_VALUE] 2209: 2210: ValidationRule ::= SimpleType | ComplexRule | UnionType 2211: 2212: SimpleType ::= string | integer | number | scalar | scalarref | stringref | arrayref | hashref | coderef | object 2213: 2214: UnionType ::= seq SimpleType -- at least two members; written as type => ['a', 'b'] 2215: 2216: ComplexRule == [ 2217: type: SimpleType | UnionType; 2218: min: ℕ₁; 2219: max: ℕ₁; 2220: optional: 𝔹; 2221: matches: REGEX; 2222: regex: REGEX; 2223: nomatch: REGEX; 2224: memberof: seq VALUE; 2225: enum: seq VALUE; 2226: values: seq VALUE; 2227: notmemberof: seq VALUE; 2228: callback: FUNCTION; 2229: isa: TYPE_NAME; 2230: can: METHOD_NAME 2231: ] 2232: 2233: Schema == PARAM_NAME ⇸ ValidationRule 2234: 2235: Arguments == PARAM_NAME ⇸ VALUE 2236: 2237: ValidatedResult == PARAM_NAME ⇸ VALUE 2238: 2239: ∀ rule: ComplexRule • 2240: rule.min ≤ rule.max ∧ 2241: ¬((rule.memberof ∨ rule.enum ∨ rule.values) ∧ rule.min) ∧ 2242: ¬((rule.memberof ∨ rule.enum ∨ rule.values) ∧ rule.max) ∧ 2243: ¬(rule.notmemberof ∧ rule.min) ∧ 2244: ¬(rule.notmemberof ∧ rule.max) 2245: 2246: ∀ schema: Schema; args: Arguments • 2247: dom(validate_strict(schema, args)) ⊆ dom(schema) ∪ dom(args) 2248: 2249: validate_strict: Schema × Arguments → ValidatedResult 2250: 2251: ∀ schema: Schema; args: Arguments • 2252: let result == validate_strict(schema, args) • 2253: (∀ name: dom(schema) ∩ dom(args) • 2254: name ∈ dom(result) ⇒ 2255: type_matches(result(name), schema(name))) ∧ 2256: (∀ name: dom(schema) • 2257: ¬optional(schema(name)) ⇒ name ∈ dom(args)) 2258: 2259: type_matches: VALUE × ValidationRule → 𝔹 2260: 2261: =head1 EXAMPLE 2262: 2263: use Params::Get; 2264: use Params::Validate::Strict; 2265: 2266: sub where_am_i 2267: { 2268: my $params = Params::Validate::Strict::validate_strict({ 2269: args => Params::Get::get_params(undef, \@_), 2270: description => 'Print a string of latitude and longitude', 2271: error_msg => 'Latitude is a number between +/- 90, longitude is a number between +/- 180', 2272: members => { 2273: 'latitude' => { 2274: type => 'number', 2275: min => -90, 2276: max => 90 2277: }, 'longitude' => { 2278: type => 'number', 2279: min => -180, 2280: max => 180 2281: } 2282: } 2283: }); 2284: 2285: print 'You are at ', $params->{'latitude'}, ', ', $params->{'longitude'}, "\n"; 2286: } 2287: 2288: where_am_i({ latitude => 3.14, longitude => -155 }); 2289: 2290: =head1 BUGS 2291: 2292: =head1 SEE ALSO 2293: 2294: =over 4 2295: 2296: =item * L<Test Dashboard|https://nigelhorne.github.io/Params-Validate-Strict/coverage/> 2297: 2298: =item * L<Data::Processor> 2299: 2300: =item * L<Params::Get> 2301: 2302: =item * L<Params::Smart> 2303: 2304: =item * L<Params::Validate> 2305: 2306: =item * L<Return::Set> 2307: 2308: =item * L<App::Test::Generator> 2309: 2310: =back 2311: 2312: =head1 SUPPORT 2313: 2314: This module is provided as-is without any warranty. 2315: 2316: Please report any bugs or feature requests to C<bug-params-validate-strict at rt.cpan.org>, 2317: or through the web interface at 2318: L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Params-Validate-Strict>. 2319: I will be notified, and then you'll 2320: automatically be notified of progress on your bug as I make changes. 2321: 2322: You can find documentation for this module with the perldoc command. 2323: 2324: perldoc Params::Validate::Strict 2325: 2326: You can also look for information at: 2327: 2328: =over 4 2329: 2330: =item * MetaCPAN 2331: 2332: L<https://metacpan.org/dist/Params-Validate-Strict> 2333: 2334: =item * RT: CPAN's request tracker 2335: 2336: L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Params-Validate-Strict> 2337: 2338: =item * CPAN Testers' Matrix 2339: 2340: L<http://matrix.cpantesters.org/?dist=Params-Validate-Strict> 2341: 2342: =item * CPAN Testers Dependencies 2343: 2344: L<http://deps.cpantesters.org/?module=Params::Validate::Strict> 2345: 2346: =back 2347: 2348: =head1 LICENSE AND COPYRIGHT 2349: 2350: Copyright 2025-2026 Nigel Horne. 2351: 2352: This program is released under the following licence: GPL2. 2353: If you use it, 2354: please let me know. 2355: 2356: =cut 2357: 2358: 1; 2359: 2360: __END__