View | Details | Raw Unified | Return to bug 34430
Collapse All | Expand All

(-)a/Koha/Validator.pm (+342 lines)
Line 0 Link Here
1
package Koha::Validator;
2
3
# Copyright 2023 Rijksmuseum, Koha development team
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
=head1 NAME
21
22
Koha::Validator - Facilitate validation of sets of values
23
24
=head1 SYNOPSIS
25
26
    use Koha::Validator;
27
    #TODO
28
29
=head1 DESCRIPTION
30
31
    This modules allows you to load rule sets using predefined rules,
32
    regular expressions and validation routines, and pass sets of values
33
    in order to apply those rules to them.
34
35
=cut
36
37
use Modern::Perl;
38
use Data::Dumper qw/Dumper/;    #FIXME
39
use List::Util   qw(any);
40
use Try::Tiny;
41
42
use Koha::Exceptions;
43
44
use constant OPTIONS => qw/ allow_missing_rules break_on_error replace_inline restrict_postfilter throw_exception /;
45
46
our $standard_rules = {};
47
48
=head1 METHODS
49
50
=head2 new
51
52
    Create validator object. Loads standard rules only.
53
54
=cut
55
56
sub new {
57
    my ($class) = @_;
58
    my $self    = bless { _errors => {}, _rules => {} }, $class;
59
    return $self->option( reset => 1 )->load($standard_rules);
60
}
61
62
=head2 load
63
64
    $validator->load( $rules );
65
66
    Load rules. Overwrite existing ones.
67
68
=cut
69
70
sub load {
71
    my ( $self, $rules ) = @_;
72
    if ( ref($rules) eq 'HASH' ) {
73
        $self->{_rules} = { %{ $self->{_rules} }, %$rules };
74
    } else {
75
        Koha::Exceptions::BadParameter->throw( parameter => 'rules' );
76
    }
77
    return $self->_replace_aliases;
78
}
79
80
=head2 unload
81
82
    $validator->unload;
83
84
    Unload custom rules, reload standard rules.
85
86
=cut
87
88
sub unload {
89
    my ($self) = @_;
90
    $self->{_rules} = {};
91
    return $self->load($standard_rules);
92
}
93
94
=head2 check
95
96
    $validator->check( [ 1, 2, 3 ] );
97
    $validator->check( { a => 1, b => 2, c => 3 } );
98
99
    Apply loaded rules to a set of values (in arrayref or hashref).
100
    Returns either validated results (ref) or undef. But if throw_exception
101
    is set, a validation error will trigger an exception.
102
    If replace_inline is set, the passed reference will be modified too.
103
104
=cut
105
106
sub check {
107
    my ( $self, $values ) = @_;
108
    $self->{_errors} = {};
109
    if ( ref($values) eq 'HASH' ) {
110
        return $self->_check_hash($values);
111
    } elsif ( ref($values) eq 'ARRAY' ) {
112
        return $self->_check_list($values);
113
    }
114
    Koha::Exceptions::BadParameter->throw( parameter => 'values' );
115
}
116
117
=head2 option
118
119
    $value = $validator->option('some_option');    # get
120
    $validator->option( reset => 1 );              # reset to default
121
    $validator->option($hash);                     # set
122
    $validator->option(%hash);                     # set
123
124
    Get or set validator options.
125
126
=cut
127
128
sub option {
129
    my ( $self, @args ) = @_;
130
    if ( @args == 1 ) {
131
        if ( !ref( $args[0] ) ) {
132
            return ( any { $_ eq $args[0] } OPTIONS ) ? $self->{ '_' . $args[0] } : undef;
133
        } elsif ( ref( $args[0] ) eq 'HASH' ) {
134
            return $self->_set_opts( $args[0] );
135
        }
136
    } elsif ( @args == 2 && $args[0] eq 'reset' ) {    # reset to default values
137
        $self->{_allow_missing_rules} = 0;
138
        $self->{_break_on_error}      = 1;
139
        $self->{_restrict_postfilter} = 1;
140
        $self->{_replace_inline}      = 0;
141
        $self->{_throw_exception}     = 1;
142
        return $self;
143
    } elsif ( @args % 2 == 0 ) {
144
        return $self->_set_opts( {@args} );
145
    }
146
    Koha::Exceptions::BadParameter->throw( parameter => [@args] );
147
}
148
149
=head2 errors
150
151
    $count = $validator->errors;
152
    $code  = $validator->errors('field_name');
153
    $hash  = $validator->errors( hash         => 1 );       # { field1 => code1, ... }
154
    $list  = $validator->errors( code         => CODE );    # [ field1, ... ]
155
    $list  = $validator->errors( array | list => 1 );       # [ field1, ... ]
156
157
    Returns error information.
158
159
=cut
160
161
sub errors {
162
    my ( $self, @args ) = @_;
163
    if ( @args == 0 ) {
164
        return scalar keys %{ $self->{_errors} };
165
    } elsif ( @args == 1 ) {
166
        return $self->{_errors}->{ $args[0] };
167
    } elsif ( @args == 2 && $args[0] eq 'code' ) {
168
        return [ grep { $self->{_errors}->{$_} eq $args[1] } sort keys %{ $self->{_errors} } ];
169
    } elsif ( @args == 2 && $args[0] eq 'hash' ) {
170
        return { %{ $self->{_errors} } };    # clone
171
    } elsif ( @args == 2 && ( $args[0] eq 'list' || $args[0] eq 'array' ) ) {
172
        return [ sort keys %{ $self->{_errors} } ];
173
    }
174
    Koha::Exceptions::BadParameter->throw( parameter => [@args] );
175
}
176
177
=head1 INTERNAL ROUTINES
178
179
=cut
180
181
sub _replace_aliases {
182
    my $self = shift;
183
    foreach my $key ( keys %{ $self->{_rules} } ) {
184
        my $rule = $self->{_rules}->{$key};
185
        next if ref($rule) eq 'HASH';
186
        if ( $rule && ref( $self->{_rules}->{$rule} ) eq 'HASH' ) {
187
            $self->{_rules}->{$key} = $self->{_rules}->{$rule};
188
        } else {
189
            Koha::Exceptions::BadParameter->throw("Bad rule for $key");
190
        }
191
    }
192
    return $self;
193
}
194
195
sub _set_opts {
196
    my ( $self, $options ) = @_;
197
    foreach my $key ( keys %$options ) {
198
        next unless any { $_ eq $key } OPTIONS;    # just ignores unknown options
199
        $self->{"_$key"} = $options->{$key} ? 1 : 0;
200
    }
201
    return $self;
202
}
203
204
sub _check_hash {
205
    my ( $self, $values ) = @_;
206
207
    return unless $self->_check_mandatory($values) || !$self->{_break_on_error};
208
209
    my $results = {};
210
    foreach my $key ( sort keys %$values ) {
211
        my $rule  = exists $self->{_rules}->{$key} ? $self->{_rules}->{$key} : $self->{_rules}->{defaultrule};
212
        my $value = $values->{$key};
213
        $results->{$key} = $self->_check_value( $rule, $results, $value, $key );
214
        last if exists $self->{_errors}->{$key} && $self->{_break_on_error};
215
    }
216
    return $self->_handle_record_validation_error if scalar %{ $self->{_errors} };
217
    $self->_replace_hash( $results, $values )     if $self->{_replace_inline};
218
    return $results;
219
}
220
221
sub _check_mandatory {
222
    my ( $self, $values ) = @_;
223
    foreach my $key ( keys %{ $self->{_rules} } ) {
224
        next unless ref( $self->{_rules}->{$key} ) eq 'HASH' && $self->{_rules}->{$key}->{mandatory};
225
        if ( !exists $values->{$key} ) {
226
            $self->_handle_validation_error( $key, undef, 'MANDATORY' );
227
            return if $self->{_break_on_error};
228
        }
229
    }
230
    return !scalar %{ $self->{_errors} };
231
}
232
233
sub _handle_record_validation_error {
234
    my $self = shift;
235
236
    # coming here implies that break_on_error is false
237
    if ( $self->{_throw_exception} ) {
238
        Koha::Exceptions::BadParameter->throw( parameter => $self->errors( hash => 1 ) );
239
    }
240
    return;
241
}
242
243
sub _replace_hash {
244
    my ( $self, $results, $values ) = @_;
245
    map { $values->{$_} = $results->{$_} } keys %$results;
246
}
247
248
sub _check_list {
249
    my ( $self, $values ) = @_;
250
251
    my $index   = 0;
252
    my $results = [];
253
    foreach my $value (@$values) {
254
        my $rule = $self->{_rules}->{defaultrule};
255
        $results->[$index] = $self->_check_value( $rule, $results, $value, $index );
256
        last if exists $self->{_errors}->{$index} && $self->{_break_on_error};
257
        $index++;
258
    }
259
    return $self->_handle_record_validation_error if scalar %{ $self->{_errors} };
260
    $self->_replace_list( $results, $values )     if $self->{_replace_inline};
261
    return $results;
262
}
263
264
sub _replace_list {
265
    my ( $self, $results, $values ) = @_;
266
    splice @$values, 0, scalar @$values, @$results;
267
}
268
269
sub _check_value {
270
    my ( $self, $rule, $results, $value, $index ) = @_;
271
272
    my ( $res, $code );
273
274
    # Is rule defined, should it exist?
275
    if ( defined($rule) ) {
276
        $res = ref($rule) eq 'HASH' ? 1 : 0;
277
    } else {
278
        $res  = $self->{_allow_missing_rules};
279
        $rule = {};                              # prevent warns
280
    }
281
    $code = 'RULE' unless $res;
282
283
    # Check type of value?
284
    if ( $res && exists $rule->{ref} ) {
285
        my $type = $rule->{ref} // q{};
286
        $res  = ref($type) eq q{} && ref($value) eq $type;
287
        $code = 'REF' unless $res;
288
    }
289
290
    # Insert defaultvalue?
291
    if ( $res && exists $rule->{defaultvalue} ) {
292
        $value = $rule->{defaultvalue} if !defined($value);
293
    }
294
295
    if ( $res && exists $rule->{prefilter} ) {
296
        $res = ref( $rule->{prefilter} ) eq 'CODE';
297
        try { $value = $rule->{prefilter}->( $value, $index ) }
298
        catch { $res = 0 }
299
        if $res;
300
        $code = 'PREFILTER' unless $res;
301
    }
302
303
    # Validation: values, regex and code
304
    if ( $res && exists $rule->{values} ) {
305
        $res  = ref( $rule->{values} ) eq 'ARRAY' && any { $_ eq $value } @{ $rule->{values} };
306
        $code = 'VALUES' unless $res;
307
    }
308
    if ( $res && exists $rule->{regexp} ) {
309
        $res  = ref( $rule->{regexp} ) eq 'Regexp' && $value =~ $rule->{regexp};
310
        $code = 'REGEXP' unless $res;
311
    }
312
    if ( $res && exists $rule->{code} ) {
313
        $res = ref( $rule->{code} ) eq 'CODE';
314
        try { $res = $rule->{code}->( $value, $index ) }
315
        catch { $res = 0 }
316
        if $res;
317
        $code = 'CODE' unless $res;
318
    }
319
320
    if ( $res && exists $rule->{postfilter} ) {
321
        if ( $res = ref( $rule->{postfilter} ) eq 'CODE' ) {
322
            my $data;
323
            $data = ref($results) eq 'HASH' ? {%$results} : [@$results] if $self->{_restrict_postfilter};    #clone
324
            try { $value = $rule->{postfilter}->( $value, $index, $data // $results ) }
325
            catch { $res = 0 };
326
        }
327
        $code = 'POSTFILTER' unless $res;
328
    }
329
330
    $self->_handle_validation_error( $index, $value, $code ) unless $res;
331
    return $value;
332
}
333
334
sub _handle_validation_error {
335
    my ( $self, $index, $value, $code ) = @_;
336
    $self->{_errors}->{$index} = $code;
337
    if ( $self->{_throw_exception} && $self->{_break_on_error} ) {
338
        Koha::Exceptions::BadParameter->throw( parameter => [ $index, $value, $code ] );
339
    }
340
}
341
342
1;
(-)a/t/Koha/Validator.t (-1 / +379 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# Copyright 2023 Rijksmuseum
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
use Data::Dumper qw( Dumper );
22
use Test::More tests => 5;
23
use Test::Exception;
24
use Test::Warn;
25
use Try::Tiny;
26
27
use Koha::Validator;
28
use Koha::Email;
29
30
our $validator;
31
32
subtest 'new' => sub {
33
    plan tests => 2;
34
35
    $validator = Koha::Validator->new;
36
    is( ref($validator),                       'Koha::Validator', 'Object type' );
37
    is( $validator->option('throw_exception'), 1,                 'Check if option has default value' );
38
};
39
40
subtest 'option' => sub {
41
    plan tests => 10;
42
43
    is( ref( $validator->option ),            'Koha::Validator', 'Called option without parameters' );
44
    is( $validator->option('a'),              undef,             'Check unknown option' );
45
    is( $validator->option('break_on_error'), 1,                 'Check option break_on_error' );
46
    is( $validator->option('replace_inline'), 0,                 'Check option replace_inline' );
47
48
    is( ref $validator->option( a => 1 ), 'Koha::Validator', 'Trying to set unknown option' );
49
    is( $validator->option('a'),          undef,             'Check unknown option again' );
50
51
    $validator->option( a => 1, b => 2, replace_inline => 3, REPLACE_INLINE => 0 );    # last uc option incorrect
52
    is( $validator->option('replace_inline'), 1, 'Check option replace_inline after setting with hash' );
53
    $validator->option( { a => 1, b => 2, break_on_error => 0, replace_inline => undef } );
54
    is( $validator->option('replace_inline'), 0, 'Check option replace_inline after setting with hashref' );
55
    is( $validator->option( reset => 1 )->option('break_on_error'), 1, 'break_on_error has been reset' );
56
    throws_ok { $validator->option( a => 1, b => 2, 3 ) } 'Koha::Exceptions::BadParameter', 'Bad number of parameters';
57
};
58
59
subtest 'load' => sub {
60
    plan tests => 15;
61
62
    throws_ok { $validator->load } 'Koha::Exceptions::BadParameter',
63
        'No input for load';
64
    throws_ok { $validator->load('wrong') } 'Koha::Exceptions::BadParameter',
65
        'Wrong input for load';
66
    throws_ok { $validator->load( { defaultrule => undef } ) } 'Koha::Exceptions::BadParameter',
67
        'Rule should not be undefined';
68
    throws_ok { $validator->load( { defaultrule => 'test' } ) } 'Koha::Exceptions::BadParameter',
69
        'The alias \'test\' cannot be resolved';
70
71
    $validator->option( throw_exception => 0, allow_missing_rules => 1 );
72
73
    is( ref $validator->load( { defaultrule => {} } ), 'Koha::Validator', 'Load returned object' );
74
    ok( $validator->check( { 1 => 2 } ), 'Checked hash with empty defaultrule' );
75
    ok( $validator->check( [ 1, 2 ] ),   'Checked array with empty defaultrule' );
76
    $validator->load( { defaultrule => { code => sub { 1 } } } );
77
    ok( $validator->check( { 1 => 2 } ), 'Checked hash with true sub' );
78
    ok( $validator->check( [ 1, 2 ] ),   'Checked array with true sub' );
79
    $validator->load( { defaultrule => { code => sub { } } } );
80
    ok( !$validator->check( { 1 => 2 } ), 'Checked hash with false sub' );
81
    ok( !$validator->check( [ 1, 2 ] ),   'Checked array with false sub' );
82
83
    $validator->unload->load( { my_column => { values => [ 2, 3 ] } } );
84
    ok( $validator->check( [1] ),                'Checked array without defaultrule' );
85
    ok( $validator->check( { my_column => 3 } ), 'Checked hash with single rule' );
86
87
    # Add standard rules, overwrite
88
    $Koha::Validator::standard_rules = { rule1 => { regexp => qr/\D/ }, rule2 => { defaultvalue => 1 } };
89
    ok( $validator->unload->check( { rule1 => '2a' } ), 'Standard rule applied' );
90
    $validator->load( { rule1 => { postfilter => sub { 2 } } } );
91
    is_deeply(
92
        $validator->check( { rule1 => '2a', rule2 => undef } ), { rule1 => 2, rule2 => 1 },
93
        'Rule1 overwritten'
94
    );
95
    $Koha::Validator::standard_rules = {};
96
97
    $validator->unload->option( reset => 1 );
98
};
99
100
subtest 'check' => sub {
101
    plan tests => 19;
102
103
    throws_ok { $validator->check } 'Koha::Exceptions::BadParameter', 'No values to check';
104
    throws_ok { $validator->check(1) } 'Koha::Exceptions::BadParameter', 'Check does only accept hashref or arrayref';
105
    ok( $validator->check( {} ), 'Empty hash validated' );
106
    ok( $validator->check( [] ), 'Empty list validated' );
107
108
    subtest 'arrayref' => sub {
109
        $validator->load( { defaultrule => { values => [ 1, 2, 3 ] } } )->option( throw_exception => 0 );
110
        ok( $validator->check( [ 1, 2, '2', 3, '1' ] ), 'Check array of integers and characters with defaultrule' );
111
112
        my $code = sub {
113
            my ( $value, $index ) = @_;
114
            return ( $value == 2 or $value == 3 ) if $index == 0;
115
            return $value == 2                    if $index == 1;
116
            return $value =~ qr/test/             if $index == 2;
117
            return 0                              if $index == 3;
118
            return 0;
119
        };
120
        $validator->load( { defaultrule => { ref => '', code => $code } } );
121
122
        ok( !$validator->check( [ 1, ] ), 'Checked array with one element' );
123
        is( $validator->errors(0), 'CODE', 'Check error' );
124
        ok( $validator->check( [ 3, ] ),    'Checked another array with one element' );
125
        ok( !$validator->check( [ [1] ] ),  'Checked another array with one nested arrayref' );
126
        ok( !$validator->check( [ 2, 3 ] ), 'Checked array with two elements, fallback fails on index 1' );
127
        is( $validator->errors(1), 'CODE', 'Check error for two elements array' );
128
        ok( $validator->check( [ 2,  '2', 'testcase' ] ), 'Checked array with three elements' );
129
        ok( !$validator->check( [ 2, '2', 'textcase' ] ), 'Checked another array with three elements' );
130
        is( $validator->errors(2), 'CODE', 'Check error for three elements array' );
131
        ok( !$validator->check( [ 2, '2', 'testcase', 'index3' ] ), 'Checked array with four elements' );
132
        is( $validator->errors(3), 'CODE', 'Check error for four elements array' );
133
134
        $validator->unload->option( reset => 1 );
135
    };
136
137
    subtest 'hashref' => sub {
138
        my $rules = {
139
            digits_only => { regexp => qr/^\d+$/ },
140
            email       => { code   => sub { Koha::Email->is_valid( $_[0] ) } },
141
            test01      => 'digits_only',
142
            test02      => { values => [ 1, 2 ] },
143
            test03      => { code   => sub { 0 } },
144
            test04      => 'email',
145
            test05      => { regexp => qr/test/ },
146
        };
147
        $validator->load($rules);
148
        my $data = { test01 => 2, test02 => 1, test04 => 'marcel@test.nl', test05 => 'includes test word' };
149
        try { $validator->check($data); ok( 1, 'validator green' ) }
150
        catch { ok(0) };
151
        is( $validator->errors, 0, 'No errors encountered' );
152
153
        $data = { test01 => 2, test02 => 1, test03 => 'nomatterwhat' };
154
        try { $validator->check($data); ok(0) }
155
        catch { ok( 1, 'validator red' ) };
156
        is( $validator->errors('test03'), 'CODE', 'Check error code for test03' );
157
158
        my $code = sub { return Koha::Email->is_valid( $_[0] ) && $_[0] !~ /[\`]/ };
159
        $validator->load( { defaultrule => 'email', email => { code => $code } } );
160
        $data = { email => 'withbacktick`ls`@test.com' };
161
        try { $validator->check($data); ok(0) }
162
        catch { is( $validator->errors('email'), 'CODE', 'validator red for email with backticks' ) };
163
        ok( $validator->check( { emailpro => 'john@doe.com' } ), 'John Doe never fails' );
164
        $validator->unload->option( reset => 1 );
165
    };
166
167
    subtest 'mandatory' => sub {
168
        $validator->option( throw_exception => 0 );
169
        $validator->load( { test => { mandatory => 1 }, test2 => {}, test3 => { mandatory => 0 } } );
170
        ok( !$validator->check( {} ), 'Missing test column' );
171
        is( $validator->errors('test'), 'MANDATORY', 'Check field' );
172
        ok( $validator->check( { test => 1, test2 => 0 } ), 'only test should be present' );
173
        $validator->unload->option( reset => 1 );
174
    };
175
176
    subtest 'ref' => sub {
177
        $validator->option( break_on_error => 0, throw_exception => 0 );
178
        $validator->load( { a => { ref => q{} }, b => { ref => 'HASH' }, c => {}, d => { ref => [1] } } );
179
        ok( !$validator->check( { a => [], b => q{}, c => 1 } ), 'a and b should fail' );
180
        is_deeply( $validator->errors( code => 'REF' ), [ 'a', 'b' ], 'Check wrong fields' );
181
        ok( !$validator->check( { d => 1 } ), 'Wrong ref definition, scalar expected' );
182
        $validator->unload->option( reset => 1 );
183
    };
184
185
    subtest 'defaultvalue' => sub {
186
        $validator->load( { defaultrule => { defaultvalue => 1 } } );
187
        is_deeply( $validator->check( [ undef, q{}, 0, 2 ] ),     [ 1, q{}, 0, 2 ],     'defaultvalue on array' );
188
        is_deeply( $validator->check( { a => undef, b => q{} } ), { a => 1, b => q{} }, 'defaultvalue on hash' );
189
        $validator->unload;
190
    };
191
192
    subtest 'prefilter' => sub {
193
        $validator->load( { test => { prefilter => sub { $_[0] =~ s/\s//g; $_[0] } } } );
194
        is_deeply( $validator->check( { test => 'a b c ' } ), { test => 'abc' }, 'Prefilter removed spaces' );
195
        $validator->load( { test => { prefilter => undef } } );
196
        throws_ok { $validator->check( { test => 'a b c ' } ) } 'Koha::Exceptions::BadParameter',
197
            'Wrong prefilter, no coderef';
198
        $validator->unload;
199
    };
200
201
    subtest 'values' => sub {
202
        $validator->option( break_on_error => 0, throw_exception => 0 );
203
204
        # Note: values for a and b are bad definitions!
205
        $validator->load( { a => { values => undef }, b => { values => 1 }, c => { values => [ 10, 20 ] } } );
206
        $validator->check( { a => 1, b => 1, c => 1 } );
207
        is_deeply( $validator->errors( code => 'VALUES' ), [ 'a', 'b', 'c' ], 'a, b and c failed' );
208
        $validator->unload;
209
        $validator->load( { defaultrule => { values => [ 1, 2 ] }, c => { values => [3] } } );
210
        ok( $validator->check( { a => 1, b => 2, c => 3 } ), 'All passed now' );
211
212
        $validator->unload->option( reset => 1 );
213
    };
214
215
    subtest 'regexp' => sub {
216
        $validator->option( break_on_error => 0 );
217
        $validator->load( { defaultrule => { regexp => '/test/i' } } );    # bad defined
218
        try { $validator->check( [1] ); ok(0) }
219
        catch { ok( 1, 'Bad regex definition' ) };
220
        $validator->load( { defaultrule => { regexp => qr/\d\D/ } } );
221
        try { $validator->check( [ '123', '1b', 'ab2' ] ); ok(0) }
222
        catch { ok( 1, 'Should fail on some elements' ) };
223
        is_deeply( $validator->errors( code => 'REGEXP' ), [ 0, 2 ], 'Two elements failed' );
224
        $validator->unload->option( reset => 1 );
225
    };
226
227
    subtest 'code' => sub {
228
        $validator->load( { test1 => { code => undef }, test2 => { code => 1 } } );    # bad defined
229
        try { $validator->check( { test1 => 1 } ); ok(0) }
230
        catch { ok( 1, 'Should fail on code undef' ) };
231
        try { $validator->check( { test2 => 2 } ); ok(0) }
232
        catch { ok( 1, 'Should fail on code == 1' ) };
233
        $validator->load(
234
            { defaultrule => { code => sub { $_[0] =~ /a/ } }, test2 => { code => sub { $_[0] =~ /b/ } } } );
235
        my $res = $validator->check( { test => 'Aa', test2 => 'bB' } );
236
        $validator->unload;
237
    };
238
239
    subtest 'postfilter' => sub {
240
        $validator->option( throw_exception => 0 );
241
        $validator->load(
242
            {
243
                defaultrule => {
244
                    prefilter  => sub { $_[0] =~ s/\s//g; $_[0] },
245
                    postfilter => sub { uc $_[0] },
246
                },
247
                test  => {},
248
                test3 => {
249
                    postfilter => sub { die },
250
                },
251
            }
252
        );
253
        my $results = $validator->check( { test => 'a b c ', test2 => 'd e' } );
254
        is( $results->{test},  'a b c ', 'Field \'test\' does not hit postfilter' );
255
        is( $results->{test2}, 'DE',     'Test2 passed postfilter of defaultrule' );
256
257
        # what if postfilter crashes
258
        ok( !$validator->check( { test => 'a b c ', test2 => 'd e', test3 => 'f g' } ), 'Should stumble over die' );
259
        is( $validator->errors('test3'), 'POSTFILTER', 'Check which field did not validate' );
260
261
        $validator->unload->option( reset => 1 );
262
    };
263
264
    subtest 'allow_missing_rules' => sub {
265
        $validator->option( allow_missing_rules => 1, throw_exception => 0 );
266
        $validator->load( { test => { regexp => qr/\d/ } } );
267
        ok( $validator->check( { test => 2 } ), 'Validated' );
268
        ok( $validator->check( { tezt => 3 } ), 'No rule for tezt is allowed here' );
269
        $validator->option( allow_missing_rules => 0 );
270
        ok( !$validator->check( { tezt => 3 } ), 'No rule for tezt no longer allowed' );
271
        $validator->unload->option( reset => 1 );
272
    };
273
274
    subtest 'break_on_error' => sub {
275
        $validator->option( throw_exception => 0 );
276
        $validator->load( { a => { values => [ 1, 2 ] }, b => { values => [ 2, 3 ] } } );
277
        ok( !$validator->check( { a => 0, b => 0 } ), 'Wrong values for a and b' );
278
        is_deeply( $validator->errors( array => 1 ), ['a'], 'Stopped when a was wrong' );
279
        $validator->option( break_on_error => 0 );
280
        ok( !$validator->check( { a => 0, b => 0 } ), 'Check a and b again' );
281
        is_deeply( $validator->errors( array => 1 ), [ 'a', 'b' ], 'Found a and b now' );
282
        $validator->unload->option( reset => 1 );
283
    };
284
285
    subtest 'replace_inline' => sub {
286
        $validator->option( replace_inline => 0, throw_exception => 0 );
287
        $validator->load( { defaultrule => { ref => q{}, postfilter => sub { $_[0] + 1 } } } );
288
289
        my $array = [ 1, 2 ];
290
        my $hash  = { a => 1, b => 2 };
291
        is_deeply( $validator->check($array), [ 2, 3 ],           'Check results array' );
292
        is_deeply( $validator->check($hash),  { a => 2, b => 3 }, 'Check results hash' );
293
        is_deeply( $array,                    [ 1, 2 ],           'Original array untouched' );
294
        is_deeply( $hash,                     { a => 1, b => 2 }, 'Original hash untouched' );
295
296
        $validator->option( replace_inline => 1 );
297
298
        # First, add an invalid argument (reference); check and test if not modified
299
        $array = [ 1, 2, {} ];
300
        $hash  = { a => 1, b => 2, c => [] };
301
        ok( !$validator->check($array), 'Array does no longer validate' );
302
        ok( !$validator->check($hash),  'Hash does no longer validate' );
303
        is_deeply( $array, [ 1, 2, {} ],                'Original array untouched' );
304
        is_deeply( $hash,  { a => 1, b => 2, c => [] }, 'Original hash untouched' );
305
306
        # Pass valid content, and test if modified
307
        $array = [ 1, 2 ];
308
        $hash  = { a => 1, b => 2 };
309
        $validator->check($array);
310
        $validator->check($hash);
311
        is_deeply( $array, [ 2, 3 ],           'Original array modified' );
312
        is_deeply( $hash,  { a => 2, b => 3 }, 'Original hash modified' );
313
314
        $validator->unload->option( reset => 1 );
315
    };
316
317
    subtest 'restrict_postfilter' => sub {
318
        # Example of 3 fields, a == b + c; note that validations starts with a..
319
        # Postfilter can only validate by throwing an exception.
320
        my $code = sub { my ( $val, $idx, $res ) = @_; die unless $res->{a} == $res->{b} + $val; $val };
321
        $validator->load( { a => {}, b => {}, c => { postfilter => $code } } );
322
        ok( $validator->check( { a => 3, b => 2, c => 1 } ), 'Passes' );
323
        try { $validator->check( { a => 4, b => 2, c => 1 } ); ok(0) }
324
        catch { ok( 1, 'Should fail' ) };
325
326
        # Now try to make postfilter correct/enforce the value of a (restrict_postfilter on)
327
        $code = sub { my ( $val, $idx, $res ) = @_; $res->{a} = $res->{b} + $val; $val };
328
        $validator->load( { a => {}, b => {}, c => { postfilter => $code } } );
329
        my $res = $validator->check( { a => 4, b => 2, c => 1 } );
330
        is( $res->{a}, 4, 'Postfilter failed to adjust the value of a' );
331
        # Correct attempt: restrict_postfilter off
332
        $validator->option( restrict_postfilter => 0 );
333
        $res = $validator->check( { a => 4, b => 2, c => 1 } );
334
        is( $res->{a}, 3, 'Postfilter did adjust the value of a, restrict_postfilter off' );
335
336
        $validator->unload->option( reset => 1 );
337
    };
338
339
    subtest 'throw_exception' => sub {
340
        try { $validator->check( [1] ); ok(0) }
341
        catch { ok( 1, 'Exception for array without defaultrule' ) };
342
        $validator->option( throw_exception => 0 );
343
        my $res;
344
        lives_ok { $res = $validator->check( [1] ) } 'No exception for same check';
345
        is( $res, undef, 'Returned undef' );
346
        $validator->unload->option( reset => 1 );
347
    };
348
};
349
350
subtest 'errors' => sub {
351
    plan tests => 14;
352
    $validator->option( throw_exception => 0 );
353
    $validator->load(
354
        {
355
            defaultrule => { ref => q{}, values => [ 1, 2 ], regexp => qr/[3-9]/ },
356
            test        => { mandatory => 1, code => sub { } }
357
        }
358
    );
359
    ok( !$validator->check( [ 0, {}, 2 ] ), 'Three elements should fail' );
360
    is( $validator->errors,    1,        'Check error count, break on' );
361
    is( $validator->errors(0), 'VALUES', 'Check error of index 0, break on' );
362
    is_deeply( $validator->errors( hash => 1 ),        { 0 => 'VALUES' }, 'Check hash of errors, break on' );
363
    is_deeply( $validator->errors( list => 1 ),        [0],               'Check list of errors, break on' );
364
    is_deeply( $validator->errors( code => 'REF' ),    [],                'Check list of errors for REF, break on' );
365
    is_deeply( $validator->errors( code => 'VALUES' ), [0],               'Check list of errors for VALUES, break on' );
366
367
    $validator->option( break_on_error => 0 );
368
    ok( !$validator->check( [ 0, {}, 2 ] ), 'Three elements should fail again' );
369
    is( $validator->errors,    3,        'Check error count, break off' );
370
    is( $validator->errors(2), 'REGEXP', 'Check error of index 2, break off' );
371
    is_deeply(
372
        $validator->errors( hash => 1 ), { 0 => 'VALUES', 1 => 'REF', 2 => 'REGEXP' },
373
        'Check hash of errors, break off'
374
    );
375
    is_deeply( $validator->errors( array => 1 ),      [ 0, 1, 2 ], 'Check array of errors, break off' );
376
    is_deeply( $validator->errors( code  => 'REF' ),  [1],         'Check list of errors for REF, break off' );
377
    is_deeply( $validator->errors( code  => 'CODE' ), [],          'Check list of errors for CODE, break off' );
378
    $validator->unload->option( reset => 1 );
379
};

Return to bug 34430