1Validation::Class::WhitUespearpeCro(n3t)ributed Perl DocVuamleindtaattiioonn::Class::Whitepaper(3)
2
3
4

NAME

6       Validation::Class::Whitepaper - Operate with Impunity
7

VERSION

9       version 7.900057
10

INTRODUCTION

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

DATA VALIDATION PROBLEMS

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

A DATA VALIDATION SOLUTION

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

WHY VALIDATION::CLASS

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

GETTING STARTED

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

ADDITIONAL INSIGHT

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

AUTHOR

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.32.0                      2020-07-28  Validation::Class::Whitepaper(3)
Impressum