diff options
Diffstat (limited to 'lib/Params/Validate/PP.pm')
-rw-r--r-- | lib/Params/Validate/PP.pm | 735 |
1 files changed, 735 insertions, 0 deletions
diff --git a/lib/Params/Validate/PP.pm b/lib/Params/Validate/PP.pm new file mode 100644 index 0000000..e766475 --- /dev/null +++ b/lib/Params/Validate/PP.pm @@ -0,0 +1,735 @@ +package Params::Validate::PP; + +use strict; +use warnings; + +our $VERSION = '1.20'; + +use Params::Validate::Constants; +use Scalar::Util 1.10 (); + +our $options; + +# Various internals notes (for me and any future readers of this +# monstrosity): +# +# - A lot of the weirdness is _intentional_, because it optimizes for +# the _success_ case. It does not really matter how slow the code is +# after it enters a path that leads to reporting failure. But the +# "success" path should be as fast as possible. +# +# -- We only calculate $called as needed for this reason, even though it +# means copying code all over. +# +# - All the validation routines need to be careful never to alter the +# references that are passed. +# +# -- The code assumes that _most_ callers will not be using the +# skip_leading or ignore_case features. In order to not alter the +# references passed in, we copy them wholesale when normalizing them +# to make these features work. This is slower but lets us be faster +# when not using them. + +# Matt Sergeant came up with this prototype, which slickly takes the +# first array (which should be the caller's @_), and makes it a +# reference. Everything after is the parameters for validation. +sub validate_pos (\@@) { + return if $Params::Validate::NO_VALIDATION && !defined wantarray; + + my $p = shift; + + my @specs = @_; + + my @p = @$p; + if ($Params::Validate::NO_VALIDATION) { + + # if the spec is bigger that's where we can start adding + # defaults + for ( my $x = $#p + 1; $x <= $#specs; $x++ ) { + $p[$x] = $specs[$x]->{default} + if ref $specs[$x] && exists $specs[$x]->{default}; + } + + return wantarray ? @p : \@p; + } + + # I'm too lazy to pass these around all over the place. + local $options ||= _get_options( ( caller(0) )[0] ) + unless defined $options; + + my $min = 0; + + while (1) { + last + unless ( + ref $specs[$min] + ? !( exists $specs[$min]->{default} || $specs[$min]->{optional} ) + : $specs[$min] + ); + + $min++; + } + + my $max = scalar @specs; + + my $actual = scalar @p; + unless ( $actual >= $min + && ( $options->{allow_extra} || $actual <= $max ) ) { + my $minmax = ( + $options->{allow_extra} + ? "at least $min" + : ( $min != $max ? "$min - $max" : $max ) + ); + + my $val = $options->{allow_extra} ? $min : $max; + $minmax .= $val != 1 ? ' were' : ' was'; + + my $called = _get_called(); + + $options->{on_fail}->( "$actual parameter" + . ( $actual != 1 ? 's' : '' ) . " " + . ( $actual != 1 ? 'were' : 'was' ) + . " passed to $called but $minmax expected\n" ); + } + + my $bigger = $#p > $#specs ? $#p : $#specs; + foreach ( 0 .. $bigger ) { + my $spec = $specs[$_]; + + next unless ref $spec; + + if ( $_ <= $#p ) { + _validate_one_param( + $p[$_], \@p, $spec, + 'Parameter #' . ( $_ + 1 ) . ' (%s)' + ); + } + + $p[$_] = $spec->{default} if $_ > $#p && exists $spec->{default}; + } + + _validate_pos_depends( \@p, \@specs ); + + foreach ( + grep { + defined $p[$_] + && !ref $p[$_] + && ref $specs[$_] + && $specs[$_]{untaint} + } 0 .. $bigger + ) { + ( $p[$_] ) = $p[$_] =~ /(.+)/; + } + + return wantarray ? @p : \@p; +} + +sub _validate_pos_depends { + my ( $p, $specs ) = @_; + + for my $p_idx ( 0 .. $#$p ) { + my $spec = $specs->[$p_idx]; + + next + unless $spec + && UNIVERSAL::isa( $spec, 'HASH' ) + && exists $spec->{depends}; + + my $depends = $spec->{depends}; + + if ( ref $depends ) { + require Carp; + local $Carp::CarpLevel = 2; + Carp::croak( + "Arguments to 'depends' for validate_pos() must be a scalar"); + } + + my $p_size = scalar @$p; + if ( $p_size < $depends - 1 ) { + my $error + = ( "Parameter #" + . ( $p_idx + 1 ) + . " depends on parameter #" + . $depends + . ", which was not given" ); + + $options->{on_fail}->($error); + } + } + return 1; +} + +sub _validate_named_depends { + my ( $p, $specs ) = @_; + + foreach my $pname ( keys %$p ) { + my $spec = $specs->{$pname}; + + next + unless $spec + && UNIVERSAL::isa( $spec, 'HASH' ) + && $spec->{depends}; + + unless ( UNIVERSAL::isa( $spec->{depends}, 'ARRAY' ) + || !ref $spec->{depends} ) { + require Carp; + local $Carp::CarpLevel = 2; + Carp::croak( + "Arguments to 'depends' must be a scalar or arrayref"); + } + + foreach my $depends_name ( + ref $spec->{depends} + ? @{ $spec->{depends} } + : $spec->{depends} + ) { + unless ( exists $p->{$depends_name} ) { + my $error + = ( "Parameter '$pname' depends on parameter '" + . $depends_name + . "', which was not given" ); + + $options->{on_fail}->($error); + } + } + } +} + +sub validate (\@$) { + return if $Params::Validate::NO_VALIDATION && !defined wantarray; + + my $p = $_[0]; + + my $specs = $_[1]; + local $options = _get_options( ( caller(0) )[0] ) unless defined $options; + + if ( ref $p eq 'ARRAY' ) { + + # we were called as validate( @_, ... ) where @_ has a + # single element, a hash reference + if ( ref $p->[0] ) { + $p = { %{ $p->[0] } }; + } + elsif ( @$p % 2 ) { + my $called = _get_called(); + + $options->{on_fail} + ->( "Odd number of parameters in call to $called " + . "when named parameters were expected\n" ); + } + else { + $p = {@$p}; + } + } + + if ( $options->{normalize_keys} ) { + $specs = _normalize_callback( $specs, $options->{normalize_keys} ); + $p = _normalize_callback( $p, $options->{normalize_keys} ); + } + elsif ( $options->{ignore_case} || $options->{strip_leading} ) { + $specs = _normalize_named($specs); + $p = _normalize_named($p); + } + + if ($Params::Validate::NO_VALIDATION) { + return ( + wantarray + ? ( + + # this is a hash containing just the defaults + ( + map { $_ => $specs->{$_}->{default} } + grep { + ref $specs->{$_} + && exists $specs->{$_}->{default} + } + keys %$specs + ), + ( + ref $p eq 'ARRAY' + ? ( + ref $p->[0] + ? %{ $p->[0] } + : @$p + ) + : %$p + ) + ) + : do { + my $ref = ( + ref $p eq 'ARRAY' + ? ( + ref $p->[0] + ? $p->[0] + : {@$p} + ) + : $p + ); + + foreach ( + grep { + ref $specs->{$_} + && exists $specs->{$_}->{default} + } + keys %$specs + ) { + $ref->{$_} = $specs->{$_}->{default} + unless exists $ref->{$_}; + } + + return $ref; + } + ); + } + + _validate_named_depends( $p, $specs ); + + unless ( $options->{allow_extra} ) { + if ( my @unmentioned = grep { !exists $specs->{$_} } keys %$p ) { + my $called = _get_called(); + + $options->{on_fail}->( "The following parameter" + . ( @unmentioned > 1 ? 's were' : ' was' ) + . " passed in the call to $called but " + . ( @unmentioned > 1 ? 'were' : 'was' ) + . " not listed in the validation options: @unmentioned\n" + ); + } + } + + my @missing; + + # the iterator needs to be reset in case the same hashref is being + # passed to validate() on successive calls, because we may not go + # through all the hash's elements + keys %$specs; +OUTER: + while ( my ( $key, $spec ) = each %$specs ) { + if ( + !exists $p->{$key} + && ( + ref $spec + ? !( + do { + + # we want to short circuit the loop here if we + # can assign a default, because there's no need + # check anything else at all. + if ( exists $spec->{default} ) { + $p->{$key} = $spec->{default}; + next OUTER; + } + } + || do { + + # Similarly, an optional parameter that is + # missing needs no additional processing. + next OUTER if $spec->{optional}; + } + ) + : $spec + ) + ) { + push @missing, $key; + } + + # Can't validate a non hashref spec beyond the presence or + # absence of the parameter. + elsif ( ref $spec ) { + my $value = defined $p->{$key} ? qq|"$p->{$key}"| : 'undef'; + _validate_one_param( + $p->{$key}, $p, $spec, + qq{The '$key' parameter (%s)} + ); + } + } + + if (@missing) { + my $called = _get_called(); + + my $missing = join ', ', map {"'$_'"} @missing; + $options->{on_fail}->( "Mandatory parameter" + . ( @missing > 1 ? 's' : '' ) + . " $missing missing in call to $called\n" ); + } + + # do untainting after we know everything passed + foreach my $key ( + grep { + defined $p->{$_} + && !ref $p->{$_} + && ref $specs->{$_} + && $specs->{$_}{untaint} + } + keys %$p + ) { + ( $p->{$key} ) = $p->{$key} =~ /(.+)/; + } + + return wantarray ? %$p : $p; +} + +sub validate_with { + return if $Params::Validate::NO_VALIDATION && !defined wantarray; + + my %p = @_; + + local $options = _get_options( ( caller(0) )[0], %p ); + + unless ($Params::Validate::NO_VALIDATION) { + unless ( exists $options->{called} ) { + $options->{called} = ( caller( $options->{stack_skip} ) )[3]; + } + + } + + if ( UNIVERSAL::isa( $p{spec}, 'ARRAY' ) ) { + return validate_pos( @{ $p{params} }, @{ $p{spec} } ); + } + else { + + # intentionally ignore the prototype because this contains + # either an array or hash reference, and validate() will + # handle either one properly + return &validate( $p{params}, $p{spec} ); + } +} + +sub _normalize_callback { + my ( $p, $func ) = @_; + + my %new; + + foreach my $key ( keys %$p ) { + my $new_key = $func->($key); + + unless ( defined $new_key ) { + die + "The normalize_keys callback did not return a defined value when normalizing the key '$key'"; + } + + if ( exists $new{$new_key} ) { + die + "The normalize_keys callback returned a key that already exists, '$new_key', when normalizing the key '$key'"; + } + + $new{$new_key} = $p->{$key}; + } + + return \%new; +} + +sub _normalize_named { + + # intentional copy so we don't destroy original + my %h = ( ref $_[0] ) =~ /ARRAY/ ? @{ $_[0] } : %{ $_[0] }; + + if ( $options->{ignore_case} ) { + $h{ lc $_ } = delete $h{$_} for keys %h; + } + + if ( $options->{strip_leading} ) { + foreach my $key ( keys %h ) { + my $new; + ( $new = $key ) =~ s/^\Q$options->{strip_leading}\E//; + $h{$new} = delete $h{$key}; + } + } + + return \%h; +} + +my %Valid = map { $_ => 1 } + qw( callbacks can default depends isa optional regex type untaint ); + +sub _validate_one_param { + my ( $value, $params, $spec, $id ) = @_; + + # for my $key ( keys %{$spec} ) { + # unless ( $Valid{$key} ) { + # $options->{on_fail} + # ->(qq{"$key" is not an allowed validation spec key}); + # } + # } + + if ( exists $spec->{type} ) { + unless ( defined $spec->{type} + && Scalar::Util::looks_like_number( $spec->{type} ) + && $spec->{type} > 0 ) { + my $msg + = "$id has a type specification which is not a number. It is "; + if ( defined $spec->{type} ) { + $msg .= "a string - $spec->{type}"; + } + else { + $msg .= "undef"; + } + + $msg + .= ".\n Use the constants exported by Params::Validate to declare types."; + + $options->{on_fail}->( sprintf( $msg, _stringify($value) ) ); + } + + unless ( _get_type($value) & $spec->{type} ) { + my $type = _get_type($value); + + my @is = _typemask_to_strings($type); + my @allowed = _typemask_to_strings( $spec->{type} ); + my $article = $is[0] =~ /^[aeiou]/i ? 'an' : 'a'; + + my $called = _get_called(1); + + $options->{on_fail}->( + sprintf( + "$id to $called was $article '@is', which " + . "is not one of the allowed types: @allowed\n", + _stringify($value) + ) + ); + } + } + + # short-circuit for common case + return + unless ( $spec->{isa} + || $spec->{can} + || $spec->{callbacks} + || $spec->{regex} ); + + if ( exists $spec->{isa} ) { + foreach ( ref $spec->{isa} ? @{ $spec->{isa} } : $spec->{isa} ) { + unless ( + do { + local $@ = q{}; + eval { $value->isa($_) }; + } + ) { + my $is = ref $value ? ref $value : 'plain scalar'; + my $article1 = $_ =~ /^[aeiou]/i ? 'an' : 'a'; + my $article2 = $is =~ /^[aeiou]/i ? 'an' : 'a'; + + my $called = _get_called(1); + + $options->{on_fail}->( + sprintf( + "$id to $called was not $article1 '$_' " + . "(it is $article2 $is)\n", _stringify($value) + ) + ); + } + } + } + + if ( exists $spec->{can} ) { + foreach ( ref $spec->{can} ? @{ $spec->{can} } : $spec->{can} ) { + unless ( + do { + local $@ = q{}; + eval { $value->can($_) }; + } + ) { + my $called = _get_called(1); + + $options->{on_fail}->( + sprintf( + "$id to $called does not have the method: '$_'\n", + _stringify($value) + ) + ); + } + } + } + + if ( $spec->{callbacks} ) { + unless ( UNIVERSAL::isa( $spec->{callbacks}, 'HASH' ) ) { + my $called = _get_called(1); + + $options->{on_fail}->( + "'callbacks' validation parameter for $called must be a hash reference\n" + ); + } + + foreach ( keys %{ $spec->{callbacks} } ) { + unless ( UNIVERSAL::isa( $spec->{callbacks}{$_}, 'CODE' ) ) { + my $called = _get_called(1); + + $options->{on_fail}->( + "callback '$_' for $called is not a subroutine reference\n" + ); + } + + my $ok; + my $e = do { + local $@ = q{}; + local $SIG{__DIE__}; + $ok = eval { $spec->{callbacks}{$_}->( $value, $params ) }; + $@; + }; + + if ( !$ok ) { + my $called = _get_called(1); + + if ( ref $e ) { + $options->{on_fail}->($e); + } + else { + my $msg = "$id to $called did not pass the '$_' callback"; + $msg .= ": $e" if length $e; + $msg .= "\n"; + $options->{on_fail}->( sprintf( $msg, _stringify($value) ) ); + } + } + } + } + + if ( exists $spec->{regex} ) { + unless ( ( defined $value ? $value : '' ) =~ /$spec->{regex}/ ) { + my $called = _get_called(1); + + $options->{on_fail}->( + sprintf( + "$id to $called did not pass regex check\n", + _stringify($value) + ) + ); + } + } +} + +{ + # if it UNIVERSAL::isa the string on the left then its the type on + # the right + my %isas = ( + 'ARRAY' => ARRAYREF, + 'HASH' => HASHREF, + 'CODE' => CODEREF, + 'GLOB' => GLOBREF, + 'SCALAR' => SCALARREF, + 'REGEXP' => SCALARREF, + ); + my %simple_refs = map { $_ => 1 } keys %isas; + + sub _get_type { + return UNDEF unless defined $_[0]; + + my $ref = ref $_[0]; + unless ($ref) { + + # catches things like: my $fh = do { local *FH; }; + return GLOB if UNIVERSAL::isa( \$_[0], 'GLOB' ); + return SCALAR; + } + + return $isas{$ref} if $simple_refs{$ref}; + + foreach ( keys %isas ) { + return $isas{$_} | OBJECT if UNIVERSAL::isa( $_[0], $_ ); + } + + # I really hope this never happens. + return UNKNOWN; + } +} + +{ + my %type_to_string = ( + SCALAR() => 'scalar', + ARRAYREF() => 'arrayref', + HASHREF() => 'hashref', + CODEREF() => 'coderef', + GLOB() => 'glob', + GLOBREF() => 'globref', + SCALARREF() => 'scalarref', + UNDEF() => 'undef', + OBJECT() => 'object', + UNKNOWN() => 'unknown', + ); + + sub _typemask_to_strings { + my $mask = shift; + + my @types; + foreach ( + SCALAR, ARRAYREF, HASHREF, CODEREF, GLOB, GLOBREF, + SCALARREF, UNDEF, OBJECT, UNKNOWN + ) { + push @types, $type_to_string{$_} if $mask & $_; + } + return @types ? @types : ('unknown'); + } +} + +{ + my %defaults = ( + ignore_case => 0, + strip_leading => 0, + allow_extra => 0, + on_fail => sub { + require Carp; + Carp::confess( $_[0] ); + }, + stack_skip => 1, + normalize_keys => undef, + ); + + *set_options = \&validation_options; + + sub validation_options { + my %opts = @_; + + my $caller = caller; + + foreach ( keys %defaults ) { + $opts{$_} = $defaults{$_} unless exists $opts{$_}; + } + + $Params::Validate::OPTIONS{$caller} = \%opts; + } + + sub _get_options { + my $caller = shift; + + if (@_) { + + return ( + $Params::Validate::OPTIONS{$caller} + ? { + %{ $Params::Validate::OPTIONS{$caller} }, + @_ + } + : { %defaults, @_ } + ); + } + else { + return ( + exists $Params::Validate::OPTIONS{$caller} + ? $Params::Validate::OPTIONS{$caller} + : \%defaults + ); + } + } +} + +sub _get_called { + my $extra_skip = $_[0] || 0; + + # always add one more for this sub + $extra_skip++; + + my $called = ( + exists $options->{called} + ? $options->{called} + : ( caller( $options->{stack_skip} + $extra_skip ) )[3] + ); + + $called = 'N/A' unless defined $called; + + return $called; +} + +sub _stringify { + return defined $_[0] ? qq{"$_[0]"} : 'undef'; +} + +1; |