File Coverage

File:blib/lib/Params/Validate/Strict.pm
Coverage:79.9%

linestmtbrancondsubtimecode
1package Params::Validate::Strict;
2
3
9
9
9
578727
7
126
use strict;
4
9
9
9
18
3
147
use warnings;
5
6
9
9
9
14
14
216
use Carp;
7
9
9
9
17
77
240
use List::Util 1.33 qw(any);    # Required for memberof validation
8
9
9
9
15
5
111
use Exporter qw(import);        # Required for @EXPORT_OK
9
9
9
9
1384
34993
159
use Params::Get 0.13;
10
9
9
9
25
4
13236
use Scalar::Util;
11
12our @ISA = qw(Exporter);
13our @EXPORT_OK = qw(validate_strict);
14
15 - 23
=head1 NAME

Params::Validate::Strict - Validates a set of parameters against a schema

=head1 VERSION

Version 0.14

=cut
24
25our $VERSION = '0.14';
26
27 - 242
=head1 SYNOPSIS

    my $schema = {
        username => { type => 'string', min => 3, max => 50 },
        age => { type => 'integer', min => 0, max => 150 },
    };

    my $input = {
         username => 'john_doe',
         age => '30',        # Will be coerced to integer
    };

    my $validated_input = validate_strict(schema => $schema, input => $input);

    if(defined($validated_input)) {
        print "Example 1: Validation successful!\n";
        print 'Username: ', $validated_input->{username}, "\n";
        print 'Age: ', $validated_input->{age}, "\n";      # It's an integer now
    } else {
        print "Example 1: Validation failed: $@\n";
    }

=head1  METHODS

=head2 validate_strict

Validates a set of parameters against a schema.

This function takes two mandatory arguments:

=over 4

=item * C<schema>

A reference to a hash that defines the validation rules for each parameter.
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.

=item * C<args> || C<input>

A reference to a hash containing the parameters to be validated.
The keys of the hash are the parameter names, and the values are the parameter values.

=back

It takes two optional arguments:

=over 4

=item * C<unknown_parameter_handler>

This parameter describes what to do when a parameter is given that is not in the schema of valid parameters.
It must be one of C<die> (the default), C<warn>, or C<ignore>.

=item * C<logger>

A logging object that understands messages such as C<error> and C<warn>.

=back

The schema can define the following rules for each parameter:

=over 4

=item * C<type>

The data type of the parameter.
Valid types are C<string>, C<integer>, C<number>, C<boolean>, C<hashref>, C<arrayref>, C<object> and C<coderef>.

=item * C<can>

The parameter must be an object that understands the method C<can>.
C<can> can be a simple scalar string of a method name,
or an arrayref of a list of method names, all of which must be supported by the object.

=item * C<isa>

The parameter must be an object of type C<isa>.

=item * C<memberof>

The parameter must be a member of the given arrayref.

=item * C<min>

The minimum length (for strings), value (for numbers) or number of keys (for hashrefs).

=item * C<max>

The maximum length (for strings), value (for numbers) or number of keys (for hashrefs).

=item * C<matches>

A regular expression that the parameter value must match.
Checks all members of arrayrefs.

=item * C<nomatch>

A regular expression that the parameter value must not match.
Checks all members of arrayrefs.

=item * C<callback>

A code reference to a subroutine that performs custom validation logic.
The subroutine should accept the parameter value as an argument and return true if the value is valid, false otherwise.

=item * C<optional>

A boolean value indicating whether the parameter is optional.
If true, the parameter is not required.
If false or omitted, the parameter is required.

=item * C<default>

Populate missing optional parameters with the specified value.
Note that this value is not validated.

  username => {
    type => 'string',
    optional => 1,
    default => 'guest'
  }

=item * C<element_type>

Extends the validation to individual elements of arrays.

  tags => {
    type => 'arrayref',
    element_type => 'number',
    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
    max => 5
  }

=item * C<error_message>

The custom error message to be used in the event of a validation failure.

  age => {
    type => 'integer',
    min => 18,
    error_message => 'You must be at least 18 years old'
  }

=item * C<schema>

You can validate nested hashrefs and arrayrefs using the C<schema> property:

    my $schema = {
        user => {
            type => 'hashref',
            schema => {
                name => { type => 'string' },
                age => { type => 'integer', min => 0 },
                hobbies => {
                    type => 'arrayref',
                    schema => { type => 'string' }, # Validate each element
                    min => 1 # At least one hobby
                }
            }
        },
        metadata => {
            type => 'hashref',
            schema => {
                created => { type => 'string' },
                tags => {
                    type => 'arrayref',
                    schema => {
                        type => 'string',
                        matches => qr/^[a-z]+$/
                    }
                }
            }
        }
    };

=back

If a parameter is optional and its value is C<undef>,
validation will be skipped for that parameter.

If the validation fails, the function will C<croak> with an error message describing the validation failure.

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.

=head1 MIGRATION FROM LEGACY VALIDATORS

=head2 From L<Params::Validate>

    # Old style
    validate(@_, {
        name => { type => SCALAR },
        age => { type => SCALAR, regex => qr/^\d+$/ }
    });

    # New style
    validate_strict(
        schema => {
            name => 'string',
            age => { type => 'integer', min => 0 }
        },
        args => { @_ }
    );

=head2 From L<Type::Params>

    # Old style
    my ($name, $age) = validate_positional \@_, Str, Int;

    # New style - requires converting to named parameters first
    my %args = (name => $_[0], age => $_[1]);
    my $validated = validate_strict(
        schema => { name => 'string', age => 'integer' },
        args => \%args
    );

=cut
243
244sub validate_strict
245{
246
178
775538
        my $params = Params::Get::get_params(undef, \@_);
247
248
178
2122
        my $schema = $params->{'schema'};
249
178
267
        my $args = $params->{'args'} || $params->{'input'};
250
178
268
        my $unknown_parameter_handler = $params->{'unknown_parameter_handler'} || 'die';
251
178
125
        my $logger = $params->{'logger'};
252
253        # Check if schema and args are references to hashes
254
178
176
        if(ref($schema) ne 'HASH') {
255
2
2
                _error($logger, 'validate_strict: schema must be a hash reference');
256        }
257
258
176
318
        if(exists($params->{'args'}) && (!defined($args))) {
259
2
2
                $args = {};
260        } elsif(ref($args) ne 'HASH') {
261
1
1
                _error($logger, 'validate_strict: args must be a hash reference');
262        }
263
264
175
175
125
191
        foreach my $key (keys %{$args}) {
265
240
249
                if(!exists($schema->{$key})) {
266
11
15
                        if($unknown_parameter_handler eq 'die') {
267
5
7
                                _error($logger, "::validate_strict: Unknown parameter '$key'");
268                        } elsif($unknown_parameter_handler eq 'warn') {
269
2
5
                                _warn($logger, "::validate_strict: Unknown parameter '$key'");
270
2
178
                                next;
271                        } elsif($unknown_parameter_handler eq 'ignore') {
272
3
4
                                if($logger) {
273
2
3
                                        $logger->debug(__PACKAGE__ . "::validate_strict: Unknown parameter '$key'");
274                                }
275
3
6
                                next;
276                        } else {
277
1
1
                                _error($logger, "::validate_strict: '$unknown_parameter_handler' unknown_parameter_handler must be one of die, warn, ignore");
278                        }
279                }
280        }
281
282
169
147
        my %validated_args;
283
169
169
99
160
        foreach my $key (keys %{$schema}) {
284
280
223
                my $rules = $schema->{$key};
285
280
195
                my $value = $args->{$key};
286
287
280
238
                if(!defined($rules)) {  # Allow anything
288
2
2
                        $validated_args{$key} = $value;
289
2
2
                        next;
290                }
291
292                # If rules are a simple type string
293
278
237
                if(ref($rules) eq '') {
294
23
26
                        $rules = { type => $rules };
295                }
296
297                # Handle optional parameters
298
278
429
                if((ref($rules) eq 'HASH') && $rules->{optional}) {
299
109
95
                        if(!exists($args->{$key})) {
300
59
60
                                if($rules->{'default'}) {
301                                        # Populate missing optional parameters with the specfied output values
302
2
2
                                        $validated_args{$key} = $rules->{'default'};
303                                }
304
59
54
                                next;   # optional and missing
305                        }
306                } elsif(!exists($args->{$key})) {
307                        # The parameter is required
308
3
8
                        _error($logger, "validate_strict: Required parameter '$key' is missing");
309                }
310
311                # Validate based on rules
312
216
186
                if(ref($rules) eq 'HASH') {
313
214
267
                        if((my $min = $rules->{'min'}) && (my $max = $rules->{'max'})) {
314
20
23
                                if($min > $max) {
315
4
8
                                        _error($logger, "validate_strict($key): min must be <= max ($min > $max)");
316                                }
317                        }
318
319
210
171
                        if($rules->{'memberof'}) {
320
13
15
                                if(my $min = $rules->{'min'}) {
321
0
0
                                        _error($logger, "validate_strict($key): min ($min) makes no sense with memberof");
322                                }
323
13
13
                                if(my $max = $rules->{'max'}) {
324
0
0
                                        _error($logger, "validate_strict($key): max ($max) makes no sense with memberof");
325                                }
326                        }
327
328
210
189
                        foreach my $rule_name (keys %$rules) {
329
394
283
                                my $rule_value = $rules->{$rule_name};
330
331
394
524
                                if($rule_name eq 'type') {
332
176
127
                                        my $type = lc($rule_value);
333
334
176
219
                                        if($type eq 'string') {
335
75
66
                                                if(ref($value)) {
336
4
4
                                                        if($rules->{'error_message'}) {
337
1
2
                                                                _error($logger, $rules->{'error_message'});
338                                                        } else {
339
3
4
                                                                _error($logger, "validate_strict: Parameter '$key' must be a string");
340                                                        }
341                                                }
342
71
80
                                                unless((ref($value) eq '') || (defined($value) && length($value))) {    # Allow undef for optional strings
343
0
0
                                                        if($rules->{'error_message'}) {
344
0
0
                                                                _error($logger, $rules->{'error_message'});
345                                                        } else {
346
0
0
                                                                _error($logger, "validate_strict: Parameter '$key' must be a string");
347                                                        }
348                                                }
349                                        } elsif($type eq 'integer') {
350
25
26
                                                if(!defined($value)) {
351
1
1
                                                        next;   # Skip if number is undefined
352                                                }
353
24
54
                                                if($value !~ /^\s*[+\-]?\d+\s*$/) {
354
1
1
                                                        if($rules->{'error_message'}) {
355
0
0
                                                                _error($logger, $rules->{'error_message'});
356                                                        } else {
357
1
3
                                                                _error($logger, "validate_strict: Parameter '$key' ($value) must be an integer");
358                                                        }
359                                                }
360
23
23
                                                $value = int($value); # Coerce to integer
361                                        } elsif($type eq 'number') {
362
18
17
                                                if(!defined($value)) {
363
2
2
                                                        next;   # Skip if string is undefined
364                                                }
365
16
22
                                                if(!Scalar::Util::looks_like_number($value)) {
366
2
3
                                                        if($rules->{'error_message'}) {
367
0
0
                                                                _error($logger, $rules->{'error_message'});
368                                                        } else {
369
2
2
                                                                _error($logger, "validate_strict: Parameter '$key' must be a number");
370                                                        }
371                                                }
372                                                # $value = eval $value; # Coerce to number (be careful with eval)
373
14
20
                                                $value = 0 + $value;    # Numeric coercion
374                                        } elsif($type eq 'arrayref') {
375
19
19
                                                if(!defined($value)) {
376
2
2
                                                        next;   # Skip if arrayref is undefined
377                                                }
378
17
23
                                                if(ref($value) ne 'ARRAY') {
379
0
0
                                                        if($rules->{'error_message'}) {
380
0
0
                                                                _error($logger, $rules->{'error_message'});
381                                                        } else {
382
0
0
                                                                _error($logger, "validate_strict: Parameter '$key' must be an arrayref, not " . ref($value));
383                                                        }
384                                                }
385                                        } elsif($type eq 'hashref') {
386
21
13
                                                if(!defined($value)) {
387
2
2
                                                        next;   # Skip if hashref is undefined
388                                                }
389
19
20
                                                if(ref($value) ne 'HASH') {
390
0
0
                                                        if($rules->{'error_message'}) {
391
0
0
                                                                _error($logger, $rules->{'error_message'});
392                                                        } else {
393
0
0
                                                                _error($logger, "validate_strict: Parameter '$key' must be an hashref");
394                                                        }
395                                                }
396                                        } elsif($type eq 'boolean') {
397
2
2
                                                if(!defined($value)) {
398
0
0
                                                        next;   # Skip if bool is undefined
399                                                }
400
2
11
                                                if(($value eq 'true') || ($value eq 'on')) {
401
0
0
                                                        $value = 1;
402                                                } elsif(($value eq 'false') || ($value eq 'off')) {
403
0
0
                                                        $value = 0;
404                                                }
405
2
3
                                                if(($value != 1) && ($value != 0)) {
406
1
1
                                                        if($rules->{'error_message'}) {
407
0
0
                                                                _error($logger, $rules->{'error_message'});
408                                                        } else {
409
1
1
                                                                _error($logger, "validate_strict: Parameter '$key' ($value) must be a boolean");
410                                                        }
411                                                }
412
1
1
                                                $value = int($value);   # Coerce to integer
413                                        } elsif($type eq 'coderef') {
414
4
6
                                                if(ref($value) ne 'CODE') {
415
1
1
                                                        if($rules->{'error_message'}) {
416
0
0
                                                                _error($logger, $rules->{'error_message'});
417                                                        } else {
418
1
2
                                                                _error($logger, "validate_strict: Parameter '$key' must be a coderef");
419                                                        }
420                                                }
421                                        } elsif($type eq 'object') {
422
10
21
                                                if(!Scalar::Util::blessed($value)) {
423
1
1
                                                        if($rules->{'error_message'}) {
424
0
0
                                                                _error($logger, $rules->{'error_message'});
425                                                        } else {
426
1
2
                                                                _error($logger, "validate_strict: Parameter '$key' must be an object");
427                                                        }
428                                                }
429                                        } else {
430
2
3
                                                _error($logger, "validate_strict: Unknown type '$type'");
431                                        }
432                                } elsif($rule_name eq 'min') {
433
47
42
                                        if(!defined($rules->{'type'})) {
434
0
0
                                                _error($logger, "validate_strict: Don't know type of '$key' to determine its minimum value $rule_value");
435                                        }
436
47
89
                                        if($rules->{'type'} eq 'string') {
437
15
15
                                                if(!defined($value)) {
438
0
0
                                                        next;   # Skip if string is undefined
439                                                }
440
15
16
                                                if(length($value) < $rule_value) {
441
2
3
                                                        if($rules->{'error_message'}) {
442
0
0
                                                                _error($logger, $rules->{'error_message'});
443                                                        } else {
444
2
5
                                                                _error($logger, "validate_strict: String parameter '$key' too short, must be at least length $rule_value");
445                                                        }
446                                                }
447                                        } elsif($rules->{'type'} eq 'arrayref') {
448
6
9
                                                if(!defined($value)) {
449
1
1
                                                        next;   # Skip if array is undefined
450                                                }
451
5
6
                                                if(ref($value) ne 'ARRAY') {
452
0
0
                                                        if($rules->{'error_message'}) {
453
0
0
                                                                _error($logger, $rules->{'error_message'});
454                                                        } else {
455
0
0
                                                                _error($logger, "validate_strict: Parameter '$key' must be an arrayref, not " . ref($value));
456                                                        }
457                                                }
458
5
5
4
9
                                                if(scalar(@{$value}) < $rule_value) {
459
1
2
                                                        if($rules->{'error_message'}) {
460
0
0
                                                                _error($logger, $rules->{'error_message'});
461                                                        } else {
462
1
2
                                                                _error($logger, "validate_strict: Parameter '$key' must be at least length $rule_value");
463                                                        }
464                                                }
465                                        } elsif($rules->{'type'} eq 'hashref') {
466
4
4
                                                if(!defined($value)) {
467
0
0
                                                        next;   # Skip if hash is undefined
468                                                }
469
4
4
3
6
                                                if(scalar(keys(%{$value})) < $rule_value) {
470
1
2
                                                        if($rules->{'error_message'}) {
471
0
0
                                                                _error($logger, $rules->{'error_message'});
472                                                        } else {
473
1
3
                                                                _error($logger, "validate_strict: Parameter '$key' must contain at least $rule_value keys");
474                                                        }
475                                                }
476                                        } elsif(($rules->{'type'} eq 'integer') || ($rules->{'type'} eq 'number')) {
477
21
19
                                                if(!defined($value)) {
478
1
1
                                                        next;   # Skip if hash is undefined
479                                                }
480
20
24
                                                if($value < $rule_value) {
481
5
8
                                                        if($rules->{'error_message'}) {
482
1
1
                                                                _error($logger, $rules->{'error_message'});
483                                                        } else {
484
4
9
                                                                _error($logger, "validate_strict: Parameter '$key' must be at least $rule_value");
485                                                        }
486                                                }
487                                        } else {
488
1
2
                                                _error($logger, "validate_strict: Parameter '$key' has meaningless min value $rule_value");
489                                        }
490                                } elsif($rule_name eq 'max') {
491
32
31
                                        if(!defined($rules->{'type'})) {
492
0
0
                                                _error($logger, "validate_strict: Don't know type of '$key' to determine its maximum value $rule_value");
493                                        }
494
32
55
                                        if($rules->{'type'} eq 'string') {
495
10
13
                                                if(!defined($value)) {
496
0
0
                                                        next;   # Skip if string is undefined
497                                                }
498
10
7
                                                if(length($value) > $rule_value) {
499
4
5
                                                        if($rules->{'error_message'}) {
500
0
0
                                                                _error($logger, $rules->{'error_message'});
501                                                        } else {
502
4
9
                                                                _error($logger, "validate_strict: String parameter '$key' too long, (" . length($value) . " characters), must be no longer than $rule_value");
503                                                        }
504                                                }
505                                        } elsif($rules->{'type'} eq 'arrayref') {
506
6
5
                                                if(!defined($value)) {
507
0
0
                                                        next;   # Skip if string is undefined
508                                                }
509
6
6
                                                if(ref($value) ne 'ARRAY') {
510
0
0
                                                        if($rules->{'error_message'}) {
511
0
0
                                                                _error($logger, $rules->{'error_message'});
512                                                        } else {
513
0
0
                                                                _error($logger, "validate_strict: Parameter '$key' must be an arrayref, not " . ref($value));
514                                                        }
515                                                }
516
6
6
6
7
                                                if(scalar(@{$value}) > $rule_value) {
517
3
3
                                                        if($rules->{'error_message'}) {
518
0
0
                                                                _error($logger, $rules->{'error_message'});
519                                                        } else {
520
3
4
                                                                _error($logger, "validate_strict: Parameter '$key' must contain no more than $rule_value items");
521                                                        }
522                                                }
523                                        } elsif($rules->{'type'} eq 'hashref') {
524
4
7
                                                if(!defined($value)) {
525
1
1
                                                        next;   # Skip if hash is undefined
526                                                }
527
3
3
1
3
                                                if(scalar(keys(%{$value})) > $rule_value) {
528
2
2
                                                        if($rules->{'error_message'}) {
529
0
0
                                                                _error($logger, $rules->{'error_message'});
530                                                        } else {
531
2
10
                                                                _error($logger, "validate_strict: Parameter '$key' must contain no more than $rule_value keys");
532                                                        }
533                                                }
534                                        } elsif(($rules->{'type'} eq 'integer') || ($rules->{'type'} eq 'number')) {
535
11
8
                                                if(!defined($value)) {
536
0
0
                                                        next;   # Skip if hash is undefined
537                                                }
538
11
10
                                                if($value > $rule_value) {
539
1
2
                                                        if($rules->{'error_message'}) {
540
0
0
                                                                _error($logger, $rules->{'error_message'});
541                                                        } else {
542
1
4
                                                                _error($logger, "validate_strict: Parameter '$key' ($value) must be no more than $rule_value");
543                                                        }
544                                                }
545                                        } else {
546
1
2
                                                _error($logger, "validate_strict: Parameter '$key' has meaningless max value $rule_value");
547                                        }
548                                } elsif($rule_name eq 'matches') {
549
20
27
                                        if(!defined($value)) {
550
1
1
                                                next;   # Skip if string is undefined
551                                        }
552
19
13
                                        eval {
553
19
92
                                                if($rules->{'type'} eq 'arrayref') {
554
3
6
3
2
14
3
                                                        my @matches = grep { /$rule_value/ } @{$value};
555
3
3
3
6
                                                        if(scalar(@matches) != scalar(@{$value})) {
556
1
1
                                                                if($rules->{'error_message'}) {
557
0
0
                                                                        _error($logger, $rules->{'error_message'});
558                                                                } else {
559
1
1
1
2
                                                                        _error($logger, "validate_strict: All members of parameter '$key' [", join(', ', @{$value}), "] must match pattern '$rule_value'");
560                                                                }
561                                                        }
562                                                } elsif($value !~ $rule_value) {
563
5
5
                                                        if($rules->{'error_message'}) {
564
0
0
                                                                _error($logger, $rules->{'error_message'});
565                                                        } else {
566
5
12
                                                                _error($logger, "validate_strict: Parameter '$key' ($value) must match pattern '$rule_value'");
567                                                        }
568                                                }
569                                        };
570
19
8703
                                        if($@) {
571
9
24
                                                _error($logger, "validate_strict: Parameter '$key' invalid regex '$rule_value': $@");
572                                        }
573                                } elsif($rule_name eq 'nomatch') {
574
8
22
                                        if($rules->{'type'} eq 'arrayref') {
575
4
11
4
2
19
4
                                                my @matches = grep { /$rule_value/ } @{$value};
576
4
5
                                                if(scalar(@matches)) {
577
2
3
                                                        if($rules->{'error_message'}) {
578
0
0
                                                                _error($logger, $rules->{'error_message'});
579                                                        } else {
580
2
2
2
6
                                                                _error($logger, "validate_strict: No member of parameter '$key' [", join(', ', @{$value}), "] must match pattern '$rule_value'");
581                                                        }
582                                                }
583                                        } elsif($value =~ $rule_value) {
584
1
2
                                                if($rules->{'error_message'}) {
585
0
0
                                                        _error($logger, $rules->{'error_message'});
586                                                } else {
587
1
2
                                                        _error($logger, "validate_strict: Parameter '$key' ($value) must not match pattern '$rule_value'");
588                                                }
589                                        }
590                                } elsif($rule_name eq 'memberof') {
591
13
14
                                        if(!defined($value)) {
592
0
0
                                                next;   # Skip if string is undefined
593                                        }
594
13
17
                                        if(ref($rule_value) eq 'ARRAY') {
595
12
23
                                                if(($rules->{'type'} eq 'integer') || ($rules->{'type'} eq 'number')) {
596
7
20
7
11
20
10
                                                        unless(List::Util::any { $_ == $value } @{$rule_value}) {
597
3
3
                                                                if($rules->{'error_message'}) {
598
0
0
                                                                        _error($logger, $rules->{'error_message'});
599                                                                } else {
600
3
3
4
7
                                                                        _error($logger, "validate_strict: Parameter '$key' ($value) must be one of ", join(', ', @{$rule_value}));
601                                                                }
602                                                        }
603                                                } else {
604
5
8
5
9
12
9
                                                        unless(List::Util::any { $_ eq $value } @{$rule_value}) {
605
2
3
                                                                if($rules->{'error_message'}) {
606
0
0
                                                                        _error($logger, $rules->{'error_message'});
607                                                                } else {
608
2
2
2
4
                                                                        _error($logger, "validate_strict: Parameter '$key' ($value) must be one of ", join(', ', @{$rule_value}));
609                                                                }
610                                                        }
611                                                }
612                                        } else {
613
1
1
                                                if($rules->{'error_message'}) {
614
0
0
                                                        _error($logger, $rules->{'error_message'});
615                                                } else {
616
1
2
                                                        _error($logger, "validate_strict: Parameter '$key' rule ($rule_value) must be an array reference");
617                                                }
618                                        }
619                                } elsif ($rule_name eq 'callback') {
620
14
16
                                        unless (defined &$rule_value) {
621
1
1
                                                _error($logger, "validate_strict: callback for '$key' must be a code reference");
622                                        }
623
13
13
                                        my $res = $rule_value->($value);
624
12
2210
                                        unless ($res) {
625
4
6
                                                if($rules->{'error_message'}) {
626
0
0
                                                        _error($logger, $rules->{'error_message'});
627                                                } else {
628
4
4
                                                        _error($logger, "validate_strict: Parameter '$key' failed custom validation");
629                                                }
630                                        }
631                                } elsif($rule_name eq 'isa') {
632
4
3
                                        if($rules->{'type'} eq 'object') {
633
3
8
                                                if(!$value->isa($rule_value)) {
634
1
1
                                                        _error($logger, "validate_strict: Parameter '$key' must be a '$rule_value' object");
635                                                }
636                                        } else {
637
1
1
                                                _error($logger, "validate_strict: Parameter '$key' has meaningless isa value $rule_value");
638                                        }
639                                } elsif($rule_name eq 'can') {
640
10
12
                                        if($rules->{'type'} eq 'object') {
641
9
13
                                                if(ref($rule_value) eq 'ARRAY') {
642                                                        # List of methods
643
4
4
3
11
                                                        foreach my $method(@{$rule_value}) {
644
6
16
                                                                if(!$value->can($method)) {
645
2
3
                                                                        _error($logger, "validate_strict: Parameter '$key' must be an object that understands the $method method");
646                                                                }
647                                                        }
648                                                } elsif(!ref($rule_value)) {
649
4
18
                                                        if(!$value->can($rule_value)) {
650
2
4
                                                                _error($logger, "validate_strict: Parameter '$key' must be an object that understands the $rule_value method");
651                                                        }
652                                                } else {
653
1
3
                                                        _error($logger, "validate_strict: 'can' rule for Parameter '$key must be either a scalar or an arrayref");
654                                                }
655                                        } else {
656
1
2
                                                _error($logger, "validate_strict: Parameter '$key' has meaningless can value $rule_value");
657                                        }
658                                } elsif($rule_name eq 'element_type') {
659
5
5
                                        if($rules->{'type'} eq 'arrayref') {
660
5
5
3
4
                                                foreach my $member(@{$value}) {
661
12
34
                                                        if($rule_value eq 'string') {
662
5
4
                                                                if(ref($member)) {
663
0
0
                                                                        if($rules->{'error_message'}) {
664
0
0
                                                                                _error($logger, $rules->{'error_message'});
665                                                                        } else {
666
0
0
                                                                                _error($logger, "$key can only contain strings");
667                                                                        }
668                                                                }
669                                                        } elsif($rule_value eq 'integer') {
670
3
5
                                                                if(ref($member) || ($member =~ /\D/)) {
671
0
0
                                                                        if($rules->{'error_message'}) {
672
0
0
                                                                                _error($logger, $rules->{'error_message'});
673                                                                        } else {
674
0
0
                                                                                _error($logger, "$key can only contain numbers (found $member)");
675                                                                        }
676                                                                }
677                                                        } elsif($rule_value eq 'number') {
678
4
12
                                                                if(ref($member) || ($member !~ /^[-+]?(\d*\.\d+|\d+\.?\d*)$/)) {
679
1
1
                                                                        if($rules->{'error_message'}) {
680
0
0
                                                                                _error($logger, $rules->{'error_message'});
681                                                                        } else {
682
1
1
                                                                                _error($logger, "$key can only contain numbers (found $member)");
683                                                                        }
684                                                                }
685                                                        }
686                                                }
687                                        } else {
688
0
0
                                                _error($logger, "validate_strict: Parameter '$key' has meaningless element_type value $rule_value");
689                                        }
690                                } elsif($rule_name eq 'optional') {
691                                        # Already handled at the beginning of the loop
692                                } elsif($rule_name eq 'default') {
693                                        # Handled earlier
694                                } elsif($rule_name eq 'error_message') {
695                                        # Handled in line
696                                } elsif($rule_name eq 'schema') {
697                                        # Nested schema Run the given schema against each element of the array
698
19
16
                                        if($rules->{'type'} eq 'arrayref') {
699
5
6
                                                if(ref($value) eq 'ARRAY') {
700
4
4
2
3
                                                        foreach my $member(@{$value}) {
701
6
21
                                                                validate_strict({ input => { $key => $member }, schema => { $key => $rule_value } });
702                                                        }
703                                                } elsif(defined($value)) {      # Allow undef for optional values
704
1
1
                                                        _error($logger, "validate_strict: nested schema: Parameter '$value' must be an arrayref");
705                                                }
706                                        } elsif($rules->{'type'} eq 'hashref') {
707
14
12
                                                if(ref($value) eq 'HASH') {
708
14
14
8
12
                                                        if(scalar keys(%{$value})) {
709
13
41
                                                                validate_strict({ input => $value, schema => $rule_value });
710                                                        }
711                                                } else {
712
0
0
                                                        _error($logger, "validate_strict: nested schema: Parameter '$value' must be an hashref");
713                                                }
714                                        } else {
715
0
0
                                                _error($logger, "validate_strict: Parameter '$key': 'schema' only supports arrayref and hashref, not $rules->{type}");
716                                        }
717                                } else {
718
1
12
                                        _error($logger, "validate_strict: Unknown rule '$rule_name'");
719                                }
720                        }
721                } elsif(ref($rules)) {
722
2
6
                        _error($logger, 'rules must be a hash reference or string');
723                }
724
725
136
157
                $validated_args{$key} = $value;
726        }
727
728
86
211
        return \%validated_args;
729}
730
731# Helper to log error or croak
732sub _error
733{
734
91
73
        my $logger = shift;
735
91
97
        my $message = join('', @_);
736
737
91
100
        my @call_details = caller(0);
738
91
1239
        if($logger) {
739
3
5
                $logger->error(__PACKAGE__, ' line ', $call_details[2], ": $message");
740        } else {
741
88
390
                croak(__PACKAGE__, ' line ', $call_details[2], ": $message");
742        }
743}
744
745# Helper to log warning or carp
746sub _warn
747{
748
2
2
        my $logger = shift;
749
2
4
        my $message = join('', @_);
750
751
2
2
        if($logger) {
752
1
2
                $logger->warn(__PACKAGE__, ": $message");
753        } else {
754
1
4
                carp(__PACKAGE__ . ": $message");
755        }
756}
757
758 - 865
=head1 AUTHOR

Nigel Horne, C<< <njh at nigelhorne.com> >>

=encoding utf-8

=head1 FORMAL SPECIFICATION

    [PARAM_NAME, VALUE, TYPE_NAME, CONSTRAINT_VALUE]

    ValidationRule ::= SimpleType | ComplexRule

    SimpleType ::= string | integer | number | arrayref | hashref | coderef | object

    ComplexRule == [
        type: TYPE_NAME;
        min: ℕ₁;
        max: ℕ₁;
        optional: 𝔹;
        matches: REGEX;
        nomatch: REGEX;
        memberof: seq VALUE;
        callback: FUNCTION;
        isa: TYPE_NAME;
        can: METHOD_NAME
    ]

    Schema == PARAM_NAME ⇸ ValidationRule

    Arguments == PARAM_NAME ⇸ VALUE

    ValidatedResult == PARAM_NAME ⇸ VALUE

    â”‚ ∀ rule: ComplexRule • rule.min ≤ rule.max
    â”‚ ∀ schema: Schema; args: Arguments •
    â”‚   dom(validate_strict(schema, args)) ⊆ dom(schema) ∪ dom(args)

    validate_strict: Schema × Arguments → ValidatedResult

    âˆ€ schema: Schema; args: Arguments •
      let result == validate_strict(schema, args) •
        (∀ name: dom(schema) ∩ dom(args) •
          name ∈ dom(result) ⇒
          type_matches(result(name), schema(name))) ∧
        (∀ name: dom(schema) •
          Â¬optional(schema(name)) ⇒ name ∈ dom(args))

    type_matches: VALUE × ValidationRule → 𝔹

=head1 BUGS

=head1 SEE ALSO

=over 4

=item * Test coverage report: L<https://nigelhorne.github.io/Params-Validate-Strict/coverage/>

=item * L<Params::Get>

=item * L<Params::Validate>

=item * L<Return::Set>

=back

=head1 SUPPORT

This module is provided as-is without any warranty.

Please report any bugs or feature requests to C<bug-params-validate-strict at rt.cpan.org>,
or through the web interface at
L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Params-Validate-Strict>.
I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.

You can find documentation for this module with the perldoc command.

    perldoc Params::Validate::Strict

You can also look for information at:

=over 4

=item * MetaCPAN

L<https://metacpan.org/dist/Params-Validate-Strict>

=item * RT: CPAN's request tracker

L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Params-Validate-Strict>

=item * CPAN Testers' Matrix

L<http://matrix.cpantesters.org/?dist=Params-Validate-Strict>

=item * CPAN Testers Dependencies

L<http://deps.cpantesters.org/?module=Params::Validate::Strict>

=back

=head1 LICENSE AND COPYRIGHT

Copyright 2025 Nigel Horne.

This program is released under the following licence: GPL2

=cut
866
8671;
868