1Validation::Class::WhitUespearpeCro(n3t)ributed Perl DocVuamleindtaattiioonn::Class::Whitepaper(3)
2
3
4
6 Validation::Class::Whitepaper - Operate with Impunity
7
9 version 7.900057
10
12 This whitepaper will serves as a guide to help readers understand the
13 common data validation issues as well as the the rationale and various
14 usage scenarios for Validation::Class.
15
16 Data validation is an important aspect of every application yet it is
17 often overlooked or neglected. Data validation should be thought of as
18 your data input firewall, a layer that exist between the user of your
19 application and the application's business objects.
20
22 The most common application security weakness is the failure to
23 properly validate input from the client or environment. Data validation
24 is important because it provides security, it allows you to ensure user
25 supplied data is formatted properly, is within length boundaries,
26 contains permitted characters and adheres to business rules.
27
28 To understand the problem domain we need to first ask ourselves:
29
30 * what is data validation? and ... is that what I've been doing?
31 * what are the common data validation requirements?
32 * what are the common use-cases where validation becomes tricky?
33
34 Data validation is the process of auditing a piece of data to ensure it
35 fits a specific criteria. Standard data validation requirements are:
36
37 * existence checking
38 * range checking
39 * type checking
40 * list-lookup checking
41 * dependency checking
42 * pattern checking
43 * custom validation checking (business logic)
44
45 Typically when designing an application we tend to name input
46 parameters in an arbitrarily fashion and validate the same data at
47 various stages during a program's execution (duplicating logic and
48 validation routines) in various places in the application stack. This
49 approach is inefficient and prone to bugs, inconsistencies and security
50 problems.
51
52 Data can be submitted to an application in various formats and it is
53 not always ideal, and the option to pre-format the data is not always
54 ideal or even possible. A few common use-cases were validation is
55 required and often fails (in a big big way) are as follows:
56
57 * handling arbitrarily and/or dynamically-named parameters
58 * handling input for batch-processing
59 * handling multi-type parameters (array or scalar depending on context)
60 * handling complex conditional validation logic
61 * handling multi-variant parameter names (aliases)
62 * handling parameter dependencies
63 * handling errors (reporting messages, localization, etc)
64
66 A better approach to data validation is to first consider each
67 parameter hitting your application as a transmission fitting a very
68 specific criteria and construct a data validation layer that operates
69 with that in mind (e.g. exactly like a network firewall). Your data
70 validation rules should act as filters which will accept or reject and
71 format the transmission for use within your application.
72
73 A proper validation framework should allow you to model data and
74 construct validation objects with a focus on structuring rules, reusing
75 common declarations, defining input filters and validating data. Its
76 main purpose should be to properly handle data input errors. It's
77 ulterior motive should be to ensure consistency and promote reuse of
78 data validation rules.
79
81 Validation::Class was built around the concept of compartmentalization
82 and re-use. That premise gave birth to the idea of persistent data
83 validation rules which exist in a class configuration which is
84 associated with a class which acts as a validation domain for related
85 validation rules.
86
87 Validation classes derived from Validation::Class are typically
88 configured using the Validation::Class sugar functions (or keywords).
89 Validation classes are typically defined using the following keywords:
90
91 * field - a data validation rule that matches an input parameter
92 * mixin - a configuration template which can be merged with a field
93 * directive - a field/mixin rule corresponding to a directive class name
94 * filter - a custom filtering routine which transforms a field value
95 * method - a self-validating sub-routine w/ associated validation profile
96
97 A data validation framework exists to handle failures, it is its main
98 function and purpose, in-fact, the difference between a validation
99 framework and a type-constraint system is how it responds to errors.
100
101 There are generally two types of errors that occur in an application,
102 user-errors which are expected and should be handled and reported so
103 that a user can correct the problem, and system-errors which are
104 unexpected and should cause the application to terminate and/or
105 handling the exception. Exception handling is the process of responding
106 to the occurrence, during computation, of exceptions (anomalous or
107 exceptional situations).
108
109 User errors and system errors are poplar opposites. It is not always
110 desired and/or appropriate to crash from a failure to validate user
111 input. The following examples should clearly display how
112 Validation::Class addresses key pain-points and handles common use-
113 cases were validation is usually quite arduous.
114
115 Dynamic Parameters
116 # handling arbitrary and/or dynamically-named parameters
117
118 package DynamicParameters;
119
120 use Validation::Class;
121
122 field email => {
123 required => 1,
124 pattern => qr/\@localhost$/
125 };
126
127 field login => {
128 required => 1,
129 min_length => 5,
130 alias => ['user']
131 };
132
133 field password => {
134 required => 1,
135 min_length => 5,
136 min_digits => 1,
137 alias => ['pass']
138 };
139
140 package main;
141
142 my $params = {
143 user => 'admin', # arbitrary
144 pass => 's3cret', # arbitrary
145 email_1 => 'admin@localhost', # dynamic created
146 email_2 => 'root@localhost', # dynamic created
147 email_3 => 'sa@localhost', # dynamic created
148 };
149
150 my $dp = DynamicParameters->new(params => $params);
151
152 $dp->proto->clone_field('email', $_)
153 for $dp->params->grep(qr/^email/)->keys
154 ;
155
156 print $dp->validate ? "OK" : "NOT OK";
157
158 1;
159
160 Batch-Processing
161 # handling input for batch-processing
162
163 package BatchProcessing;
164
165 use Validation::Class;
166
167 mixin scrub => {
168 required => 1,
169 filters => ['trim', 'strip']
170 };
171
172 field header => {
173 mixin => 'scrub',
174 options => ['name', 'email', 'contact', 'dob', 'country'],
175 multiples => 1 # handle param as a scalar or arrayref
176 };
177
178 field name => {
179 mixin => 'scrub',
180 filters => ['titlecase'],
181 min_length => 2
182 };
183
184 field email => {
185 mixin => 'scrub',
186 min_length => 3
187 };
188
189 field contact => {
190 mixin => 'scrub',
191 length => 10
192 };
193
194 field dob => {
195 mixin => 'scrub',
196 length => 8,
197 pattern => '##/##/##'
198 };
199
200 field country => {
201 mixin => 'scrub'
202 };
203
204 package main;
205
206 my $params = {
207 pasted_data => q{
208 name email contact dob country
209 john john@zuzu.com 9849688899 12/05/98 UK
210 jim kathy kjim@zuz.com 8788888888 05/07/99 India
211 Federar fed@zuzu.com 4484848989 11/21/80 USA
212 Micheal micheal@zuzu.com 6665551212 06/10/87 USA
213 Kwang Kit kwang@zuzu.com 7775551212 07/09/91 India
214 Martin jmartin@zuzu.com 2159995959 02/06/85 India
215 Roheeth roheeth@zuzu.com 9596012020 01/10/89 USA
216 }
217 };
218
219 # ... there are many ways this could be parsed and validated
220 # ... but this is simple
221
222 my $bpi = my @pasted_lines = map { s/^\s+//; $_ } split /\n/, $params->{pasted_data};
223 my @headers = split /\t/, shift @pasted_lines;
224
225 my $bp = BatchProcessing->new(params => { header => [@headers] });
226
227 # validate headers first
228
229 if ($bp->validate) {
230
231 $bp->params->clear;
232
233 $bpi--;
234
235 # validate each line, halt on first bad line
236
237 while (my $line = shift @pasted_lines) {
238
239 my @data = split /\t/, $line;
240
241 for (my $i=0; $i<@data; $i++) {
242
243 $bp->params->add($headers[$i], $data[$i]);
244
245 }
246
247 last unless $bp->validate;
248
249 $bp->params->clear;
250
251 $bpi--;
252
253 }
254
255 }
256
257 print ! $bpi ? "OK" : "NOT OK";
258
259 1;
260
261 Multi-Type Parameters
262 # handling multi-type parameters (array or scalar depending on context)
263
264 package MultiType;
265
266 use Validation::Class;
267
268 field letter_type => {
269
270 required => 1,
271 options => [ 'A' .. 'Z' ],
272 multiples => 1 # turn on multi-type processing
273
274 };
275
276 package main;
277
278 my $mt = MultiType->new;
279 my $ok = 0;
280
281 $mt->params->add(letter_type => 'A');
282
283 $ok++ if $mt->validate;
284
285 $mt->params->clear->add(letter_type => ['A', 'B', 'C']);
286
287 $ok++ if $mt->validate;
288
289 print $ok == 2 ? "OK" : "NOT OK";
290
291 1;
292
293 Complex Conditions
294 # handling complex conditional validation logic
295
296 package ComplexCondition;
297
298 use Validation::Class;
299
300 mixin scrub => {
301 required => 1,
302 filters => ['trim', 'strip']
303 };
304
305 mixin flag => {
306 length => 1,
307 options => [0, 1]
308 };
309
310 field first_name => {
311 mixin => 'scrub',
312 filters => ['titlecase']
313 };
314
315 field last_name => {
316 mixin => 'scrub',
317 filters => ['titlecase']
318 };
319
320 field role => {
321 mixin => 'scrub',
322 filters => ['titlecase'],
323 options => ['Client', 'Employee', 'Administrator'],
324 default => 'Client'
325 };
326
327 field address => {
328 mixin => 'scrub',
329 required => 0,
330 depends_on => ['city', 'state', 'zip']
331 };
332
333 field city => {
334 mixin => 'scrub',
335 required => 0,
336 depends_on => 'address'
337 };
338
339 field state => {
340 mixin => 'scrub',
341 required => 0,
342 length => '2',
343 pattern => 'XX',
344 depends_on => 'address'
345 };
346
347 field zip => {
348 mixin => 'scrub',
349 required => 0,
350 length => '5',
351 pattern => '#####',
352 depends_on => 'address'
353 };
354
355 field has_mail => {
356 mixin => 'flag'
357 };
358
359 profile 'registration' => sub {
360
361 my ($self) = @_;
362
363 # address info not required unless role is client or has_mail is true
364
365 return unless $self->validate('has_mail');
366
367 $self->queue(qw/first_name last_name/);
368
369 if ($self->param('has_mail') || $self->param('role') eq 'Client') {
370
371 # depends_on directive kinda makes city, state and zip required too
372 $self->queue(qw/+address/);
373
374 }
375
376 my $ok = $self->validate;
377
378 $self->clear_queue;
379
380 return $ok;
381
382 };
383
384 package main;
385
386 my $ok = 0;
387 my $mt;
388
389 $mt = ComplexCondition->new(
390 first_name => 'Rachel',
391 last_name => 'Green'
392 );
393
394 # defaults to client, missing address info
395 $ok++ if ! $mt->validate_profile('registration');
396
397 $mt = ComplexCondition->new(
398 first_name => 'monica',
399 last_name => 'geller',
400 role => 'employee'
401 );
402
403 # filters (pre-process) role and titlecase, as employee no address needed
404 $ok++ if $mt->validate_profile('registration');
405
406 $mt = ComplexCondition->new(
407 first_name => 'phoebe',
408 last_name => 'buffay',
409 address => '123 street road',
410 city => 'nomans land',
411 state => 'zz',
412 zip => '54321'
413 );
414
415 $ok++ if $mt->validate_profile('registration');
416
417 print $ok == 3 ? "OK" : "NOT OK";
418
419 1;
420
421 Multi-Variant Parameters
422 # handling multi-variant parameter names (aliases)
423
424 package MultiName;
425
426 use Validation::Class;
427
428 field login => {
429
430 required => 1,
431 min_length => 5, # must be 5 or more chars
432 min_alpha => 1, # must have at-least 1 alpha char
433 min_digits => 1, # must have at-least 1 digit char
434 min_symbols => 1, # must have at-least 1 non-alphanumeric char
435 alias => [
436 'signin',
437 'username',
438 'email',
439 'email_address'
440 ]
441
442 };
443
444 package main;
445
446 my $ok = 0;
447
448 # fail
449 $ok++ if ! MultiName->new(login => 'miso')->validate;
450
451 # nice
452 $ok++ if MultiName->new(login => 'm!s0_soup')->validate;
453
454 # no signin field exists, however, the alias directive pre-processing DWIM
455 $ok++ if MultiName->new(signin => 'm!s0_soup')->validate;
456
457 # process aliases
458 $ok++ if MultiName->new(params => {signin => 'm!s0_soup'})->validate;
459 $ok++ if MultiName->new(params => {username => 'm!s0_soup'})->validate;
460 $ok++ if MultiName->new(params => {email => 'm!s0_soup'})->validate;
461 $ok++ if MultiName->new(params => {email_address => 'm!s0_soup'})->validate;
462
463 print $ok == 7 ? "OK" : "NOT OK";
464
465 1;
466
467 Parameter Dependencies
468 # handling parameter dependencies
469
470 package ParamDependencies;
471
472 use Validation::Class;
473
474 mixin scrub => {
475 required => 1,
476 filters => ['trim', 'strip']
477 };
478
479 mixin flag => {
480 length => 1,
481 options => [0, 1]
482 };
483
484 field billing_address => {
485 mixin => 'scrub',
486 required => 1,
487 depends_on => ['billing_city', 'billing_state', 'billing_zip']
488 };
489
490 field billing_city => {
491 mixin => 'scrub',
492 required => 0,
493 depends_on => 'billing_address'
494 };
495
496 field billing_state => {
497 mixin => 'scrub',
498 required => 0,
499 length => '2',
500 pattern => 'XX',
501 depends_on => 'billing_address'
502 };
503
504 field billing_zip => {
505 mixin => 'scrub',
506 required => 0,
507 length => '5',
508 pattern => '#####',
509 depends_on => 'billing_address'
510 };
511
512 field shipping_address => {
513 mixin_field => 'billing_address',
514 depends_on => ['shipping_city', 'shipping_state', 'shipping_zip']
515 };
516
517 field shipping_city => {
518 mixin_field => 'billing_city',
519 depends_on => 'shipping_address'
520 };
521
522 field shipping_state => {
523 mixin_field => 'billing_state',
524 depends_on => 'shipping_address'
525 };
526
527 field shipping_zip => {
528 mixin_field => 'billing_zip',
529 depends_on => 'shipping_address'
530 };
531
532 field same_billing_shipping => {
533 mixin => 'flag'
534 };
535
536 profile 'addresses' => sub {
537
538 my ($self) = @_;
539
540 return unless $self->validate('same_billing_shipping');
541
542 # billing and shipping address always required
543 $self->validate(qw/+billing_address +shipping_address/);
544
545 # address must match if option is selected
546 if ($self->param('same_billing_shipping')) {
547
548 foreach my $param ($self->params->grep(qr/^shipping_/)->keys) {
549
550 my ($suffix) = $param =~ /^shipping_(.*)/;
551
552 my $billing = $self->param("billing_$suffix");
553 my $shipping = $self->param("shipping_$suffix");
554
555 # shipping_* must match billing_*
556 unless ($billing eq $shipping) {
557 $self->errors->add(
558 "Billing and shipping addresses do not match"
559 );
560 last;
561 }
562
563 }
564
565 }
566
567 return $self->error_count ? 0 : 1;
568
569 };
570
571 package main;
572
573 my $ok = 0;
574 my $pd;
575
576 $pd = ParamDependencies->new(
577 billing_address => '10 liberty boulevard',
578 billing_city => 'malvern',
579 billing_state => 'pa',
580 billing_zip => '19355'
581 );
582
583 # missing shipping address info
584 $ok++ if ! $pd->validate_profile('addresses');
585
586 $pd = ParamDependencies->new(
587 billing_address => '10 liberty boulevard',
588 billing_city => 'malvern',
589 billing_state => 'pa',
590 billing_zip => '19355',
591
592 shipping_address => '301 cherry street',
593 shipping_city => 'pottstown',
594 shipping_state => 'pa',
595 shipping_zip => '19464'
596 );
597
598 $ok++ if $pd->validate_profile('addresses');
599
600 $pd = ParamDependencies->new(
601 billing_address => '10 liberty boulevard',
602 billing_city => 'malvern',
603 billing_state => 'pa',
604 billing_zip => '19355',
605
606 same_billing_shipping => 1,
607
608 shipping_address => '301 cherry street',
609 shipping_city => 'pottstown',
610 shipping_state => 'pa',
611 shipping_zip => '19464'
612 );
613
614 # billing and shipping don't match
615 $ok++ if ! $pd->validate_profile('addresses');
616
617 $pd = ParamDependencies->new(
618 billing_address => '10 liberty boulevard',
619 billing_city => 'malvern',
620 billing_state => 'pa',
621 billing_zip => '19355',
622
623 same_billing_shipping => 1,
624
625 shipping_address => '10 liberty boulevard',
626 shipping_city => 'malvern',
627 shipping_state => 'pa',
628 shipping_zip => '19355'
629 );
630
631 $ok++ if $pd->validate_profile('addresses');
632
633 print $ok == 4 ? "OK" : "NOT OK";
634
635 1;
636
638 If you are looking for a simple way to get started with
639 Validation::Class, please review Validation::Class::Simple. The
640 instructions contained there are also relevant for configuring any
641 class derived from Validation::Class.
642
644 The following screencast <http://youtu.be/YCPViiB5jv0> and/or slideshow
645 <http://www.slideshare.net/slideshow/embed_code/9632123> explains what
646 Validation::Class is, why it was created, and what it has to offer.
647 Please note that this screencast and slideshow was created many moons
648 ago and some of its content may be a bit outdated.
649
651 Al Newkirk <anewkirk@ana.io>
652
654 This software is copyright (c) 2011 by Al Newkirk.
655
656 This is free software; you can redistribute it and/or modify it under
657 the same terms as the Perl 5 programming language system itself.
658
659
660
661perl v5.34.0 2022-01-21 Validation::Class::Whitepaper(3)