File Coverage

lib/Brannigan.pm
Criterion Covered Total %
statement 255 260 98.0
branch 105 128 82.0
condition 102 128 79.6
subroutine 43 45 95.5
pod 5 5 100.0
total 510 566 90.1


line stmt bran cond sub pod time code
1             package Brannigan;
2              
3             # ABSTRACT: Flexible library for validating and processing input.
4              
5 10     10   2188242 use warnings;
  10         25  
  10         569  
6 10     10   87 use strict;
  10         29  
  10         311  
7              
8 10     10   5233 use Hash::Merge;
  10         90842  
  10         46218  
9              
10             our $VERSION = "2.1";
11             $VERSION = eval $VERSION;
12              
13             =head1 NAME
14              
15             Brannigan - Flexible library for validating and processing input.
16              
17             =head1 SYNOPSIS
18              
19             use Brannigan;
20              
21             my %schema1 = ( params => ... );
22             my %schema2 = ( params => ... );
23             my %schema3 = ( params => ... );
24              
25             # use the OO interface
26             my $b = Brannigan->new();
27             $b->register_schema('schema1', \%schema1);
28             $b->register_schema('schema2', \%schema2);
29             $b->register_schema('schema3', \%schema3);
30              
31             my $rejects = $b->process('schema1', \%params);
32             if ($rejects) {
33             die $rejects;
34             }
35              
36             # %params is valid and ready for use.
37              
38             # Or use the functional interface
39             my $rejects = Brannigan::process(\%schema1, \%params);
40             if ($rejects) {
41             die $rejects;
42             }
43              
44             For a more comprehensive example, see L.
45              
46             =head1 DESCRIPTION
47              
48             Brannigan is an attempt to ease the pain of collecting, validating and processing
49             input parameters in user-facing applications. It's designed to answer both of
50             the main problems that such applications face:
51              
52             =over 2
53              
54             =item * Simple User Input
55              
56             Brannigan can validate and process simple, "flat" user input, possibly coming
57             from web forms.
58              
59             =item * Complex Data Structures
60              
61             Brannigan can validate and process complex data structures, possibly deserialized
62             from JSON or XML data sent to web services and APIs.
63              
64             =back
65              
66             Brannigan's approach to data validation is as follows: define a schema of
67             parameters and their validation rules, and let the module automatically examine
68             input parameters against this structure. Brannigan provides you with common
69             validators that are used everywhere, and also allows you to create custom
70             validators easily. This structure also defines how, if at all, the input should
71             be processed. This is akin to schema-based validations such as XSD, but much more
72             functional, and most of all flexible.
73              
74             Check the next section for an example of such a schema. Schemas can extend other
75             schemas, allowing you to be much more flexible in certain situations. Imagine you
76             have a blogging application. A base schema might define all validations and
77             processing needed in order to create a new blog post from user input. When
78             editing a post, however, some parameters that were required when creating the
79             post might not be required now, and maybe new parameters are introduced.
80             Inheritance helps you avoid repeating yourself.
81              
82             =head1 MANUAL
83              
84             Let's look at a complete usage example. Do not be alarmed by the size of these
85             schemas, as they showcases almost all features of Brannigan.
86              
87             package MyApp;
88              
89             use strict;
90             use warnings;
91             use Brannigan;
92              
93             # Create a new Brannigan object
94             my $b = Brannigan->new({ handle_unknown => "ignore" });
95              
96             # Create a custom 'forbid_words' validator that can be used in any schema.
97             $b->register_validator('forbid_words', sub {
98             my $value = shift;
99              
100             foreach (@_) {
101             return 0 if $value =~ m/$_/;
102             }
103              
104             return 1;
105             });
106              
107             # Create a schema for validating input to a create_post function
108             $b->register_schema('create_post', {
109             params => {
110             subject => {
111             required => 1,
112             length_between => [3, 40],
113             },
114             text => {
115             required => 1,
116             min_length => 10,
117             validate => sub {
118             my $value = shift;
119             return defined $value && $value =~ m/^lorem ipsum/ ? 1 : 0;
120             }
121             },
122             day => {
123             required => 0,
124             integer => 1,
125             value_between => [1, 31],
126             },
127             mon => {
128             required => 0,
129             integer => 1,
130             value_between => [1, 12],
131             },
132             year => {
133             required => 0,
134             integer => 1,
135             value_between => [1900, 2900],
136             },
137             section => {
138             required => 1,
139             integer => 1,
140             value_between => [1, 3],
141             postprocess => sub {
142             my $val = shift;
143              
144             return $val == 1 ? 'reviews' :
145             $val == 2 ? 'receips' : 'general';
146             },
147             },
148             id => {
149             required => 1,
150             exact_length => 10,
151             value_between => [1000000000, 2000000000],
152             },
153             array_of_ints => {
154             array => 1,
155             min_length => 3,
156             values => {
157             integer => 1,
158             },
159             preprocess => sub {
160             # Sometimes you'll find that input that is supposed to be
161             # an array is received as a single non-array item, most
162             # often because deserializers do not know the item should
163             # be in an array. This is common in XML inputs. A
164             # preprocess function can be used to fix that.
165             my $val = shift;
166             return [$val]
167             if defined $val && ref $val ne 'ARRAY';
168             return $val;
169             }
170             },
171             hash_of_langs => {
172             hash => 1,
173             keys => {
174             en => {
175             required => 1,
176             },
177             },
178             },
179             },
180             });
181              
182             # Create a schema for validating input to an edit_post function. The schema
183             # inherits the create_post schema with one small change.
184             $b->register_schema('edit_post', {
185             inherits_from => 'create_post',
186             params => {
187             subject => {
188             required => 0, # subject is no longer required
189             }
190             }
191             });
192              
193             # Now use Brannigan to validate input in your application:
194             sub create_post {
195             my ($self, $params) = @_;
196              
197             # Process and validate the parameters with the 'post' schema
198             my $rejects = $b->process('create_post', $params);
199              
200             if ($rejects) {
201             # Turn validation errors into a structure that fits your application
202             die list_errors($rejects);
203             }
204              
205             # Validation and processing suceeded, save the parameters to a database
206             $self->_save_post_to_db($params);
207             }
208              
209             sub edit_post {
210             my ($self, $id, $params) = @_;
211              
212             # Process and validate the parameters with the 'edit_post' schema
213             my $rejects = $b->process('edit_post', $params);
214              
215             if ($rejects) {
216             # Turn validation errors into a structure that fits your application
217             die list_errors($rejects);
218             }
219              
220             # Validation and processing succeeded, update the post in the database
221             $self->_update_post_in_db($params);
222             }
223              
224             =head2 HOW BRANNIGAN WORKS
225              
226             In essence, Brannigan works in five stages (which all boil down to one single
227             command):
228              
229             =over 5
230              
231             =item 1. SCHEMA PREPARATION
232              
233             Brannigan receives the name of a validation schema, and a hash reference of input
234             parameters. Brannigan then loads the schema and prepares it (merging it with
235             inherited schemas, if any) for later processing. Finalized schemas are cached
236             for improved performance.
237              
238             =item 2. DATA PREPROCESSING
239              
240             Brannigan invokes all C functions defined in the schema on the input
241             data, if there are any. These functions are allowed to modify the input.
242              
243             Configured default values will also be provided to their respective parameters in
244             this stage as well, if those parameters are not provided in the input.
245              
246             =item 3. DATA VALIDATION
247              
248             Brannigan invokes all validation methods defined in the schema on the input data,
249             and generates a hash reference of rejected parameters, if there were any. For
250             every parameter in this hash-ref, an array-ref of failed validations is
251             created.
252              
253             If one or more parameters failed validation, the next step (data postprocessing)
254             will be skipped.
255              
256             =item 4. DATA POSTPROCESSING
257              
258             If the previous stage (validation) did not fail, Brannigan will call every
259             C function defined in the schema. There are two types of
260             C functions:
261              
262             =over 2
263              
264             =item * parameter-specific
265              
266             These are defined on specific parameters. They get the parameter's value and
267             should return a new value for the parameter (possibly the same one, but they
268             must return a value).
269              
270             =item * global
271              
272             The schema may also have one global C function. This function gets
273             the entire parameter hash-ref as input. It is free to modify the hash-ref as
274             it sees fit. The function should not return any value.
275              
276             =back
277              
278             =item 5. FINAL RESULT
279              
280             If all input parameters passed validation, an undefined value is returned to
281             the caller. Otherwise, a hash-reference of rejects is returned. This is a
282             flattened structure where keys are "fully qualified" parameter names (meaning
283             dot notation is used for nested parameters), and values are hash-references
284             containing the validators for which the parameter had failed. For example, let's
285             look at the following rejects hash-ref:
286              
287             {
288             'subject' => { required => 1 },
289             'text' => { max_length => 500 },
290             'pictures.2' => { matches => qr!^http://! },
291             'phone.mobile' => { required => 1 }
292             }
293              
294             This hash-ref tells us:
295              
296             =over 4
297              
298             =item 1. The "subject" parameter is required but was not provided.
299              
300             =item 2. The "text" parameter was provided, but is longer than the maximum of 500
301             characters.
302              
303             =item 3. The third value of the "pictures" array does not start with "http://".
304              
305             =item 4. The "mobile" key of the "phone" hash parameter was not provided.
306              
307             =back
308              
309             =back
310              
311             =head2 HOW SCHEMAS LOOK
312              
313             The validation/processing schema defines the structure of the data you're
314             expecting to receive, along with information about the way it should be
315             validated and processed. Schemas are created by passing them to the Brannigan
316             constructor. You can pass as many schemas as you like, and these schemas
317             can inherit from other schemas.
318              
319             A schema is a hash-ref that contains the following keys:
320              
321             =over
322              
323             =item * inherits_from
324              
325             Either a scalar naming a different schema or an array-ref of schema names.
326             The new schema will inherit all the properties of the schema(s) defined by this
327             key. If an array-ref is provided, the schema will inherit their properties in
328             the order they are defined. See the L section for some "heads-up"
329             about inheritance.
330              
331             =item * params
332              
333             Defines the expected input. This key takes a hash-ref whose keys are the names
334             of input parameters as they are expected to be received. The values are also
335             hash references which define the necessary validation functions to assert for
336             the parameters, and other optional settings such as default values, post- and
337             pre- processing functions, and custom validation functions.
338              
339             For example, if a certain parameter, let's say 'subject', must be between 3 to 10
340             characters long, then your schema will contain:
341              
342             subject => { length_between => [3, 10] }
343              
344             If a "subject" parameter sent to your application fails the "length_between"
345             validator, then the rejects hash-ref described earlier will have the exact same
346             key-value pair as above:
347              
348             subject => { length_between => [3, 10] }
349              
350             The following extra keys can also be used in a parameter's configuration:
351              
352             B: Used to create a custom validation function for the parameter.
353             Accepts a subroutine reference. The subroutine accepts the value from the input
354             as its only parameter, and returns a boolean value indicating whether the value
355             passed the validation or not.
356              
357             For example, this custom validation function requires that the 'subject' input
358             parameter will always begin with the string "lorem ipsum":
359              
360             subject => {
361             length_between => [3, 10],
362             validate => sub {
363             my $value = shift;
364             return $value =~ m/^lorem ipsum/ ? 1 : 0;
365             }
366             }
367              
368             If a parameter fails a custom validation function, 'validate' will be added to
369             the failed validations hash-ref of the parameter in the rejects hash-ref:
370              
371             subject => {
372             length_between => [3, 10],
373             validate => 1
374             }
375              
376             B: Used to set a default value for parameters that are not required
377             and are not provided in the input hash-ref. Accepts a scalar value or a
378             subroutine reference. In the latter case, the subroutine will be called with no
379             parameters, and it should return the generated default value.
380              
381             subject => {
382             length_between => [3, 10],
383             default => 'lorem ipsum'
384             }
385              
386             # Or...
387              
388             subject => {
389             length_between => [3, 10],
390             default => sub { UUID->new->hex }
391             }
392              
393             Note that default values are given to missing parameters before the
394             validation stage, meaning they must conform with the parameters' validators.
395              
396             B: Used to process parameter values before validation functions are
397             called. This can be useful to trim leading or trailing whitespace from string
398             values, or turning scalars into arrays (a common task for XML inputs where the
399             deserializer cannot tell whether an item actually belongs in an array or not).
400             Accepts a subroutine reference with the parameter's value from the input. The
401             function must return the new value for the parameter, even if it had decided not
402             to do any actual changes.
403              
404             B: Similar to C, but happens after validation functions
405             had been called.
406              
407             subject => {
408             required => 1,
409             length_between => [3, 10],
410             preprocess => sub {
411             # Trim whitespace before validating
412             my $value = shift;
413             $value =~ s/^\s\*//;
414             $value =~ s/\s\*$//;
415             return $value;
416             }
417             validate => sub {
418             # Ensure value does not start with "lorem ipsum"
419             my $value = shift;
420             return $value =~ m/^lorem ipsum/ ? 0 : 1;
421             },
422             postprocess => sub {
423             # Lowercase the value
424             my $value = shift;
425             return lc $value;
426             }
427             }
428              
429             =item * postprocess
430              
431             Global postprocessing function. If provided, it will be called after all
432             preprocessing, input validation, and parameter-specific postprocessing had
433             completed. As opposed to parameter-specific postprocess functions, this one
434             receives the complete parameter hash-ref as its only input. It is not expected
435             to return any values. It may modify the parameter hash-ref as it sees fit.
436              
437             =back
438              
439             =head2 BUILT-IN VALIDATORS
440              
441             =head3 { required => $boolean }
442              
443             If C<$boolean> has a true value, this method will check that a required
444             parameter was indeed provided; otherwise (i.e. if C<$boolean> is not true)
445             this method will simply return a true value to indicate success.
446              
447             You should note that if a parameter is required, and a non-true value is
448             received (i.e. 0 or the empty string ""), this method considers the
449             requirement as fulfilled (i.e. it will return true). If you need to make sure
450             your parameters receive true values, take a look at the C validation
451             method.
452              
453             Please note that if a parameter is not required and indeed isn't provided
454             with the input parameters, any other validation methods defined on the
455             parameter will not be checked.
456              
457             =head3 { is_true => $boolean }
458              
459             If C<$boolean> has a true value, this method will check that C<$value>
460             has a true value (so, C<$value> cannot be 0 or the empty string); otherwise
461             (i.e. if C<$boolean> has a false value), this method does nothing and
462             simply returns true.
463              
464             =head3 { length_between => [ $min_length, $max_length ] }
465              
466             Makes sure the value's length (stringwise) is inside the range of
467             C<$min_length>-C<$max_length>, or, if the value is an array reference,
468             makes sure it has between C<$min_length> and C<$max_length> items.
469              
470             =head3 { min_length => $min_length }
471              
472             Makes sure the value's length (stringwise) is at least C<$min_length>, or,
473             if the value is an array reference, makes sure it has at least C<$min_length>
474             items.
475              
476             =head3 { max_length => $max_length }
477              
478             Makes sure the value's length (stringwise) is no more than C<$max_length>,
479             or, if the value is an array reference, makes sure it has no more than
480             C<$max_length> items.
481              
482             =head3 { exact_length => $length }
483              
484             Makes sure the value's length (stringwise) is exactly C<$length>, or,
485             if the value is an array reference, makes sure it has exactly C<$exact_length>
486             items.
487              
488             =head3 { integer => $boolean }
489              
490             If boolean is true, makes sure the value is an integer.
491              
492             =head3 { function => $boolean }
493              
494             If boolean is true, makes sure the value is a function
495             (subroutine reference).
496              
497             =head3 { value_between => [ $min_value, $max_value ] }
498              
499             Makes sure the value is between C<$min_value> and C<$max_value>.
500              
501             =head3 { min_value => $min_value }
502              
503             Makes sure the value is at least C<$min_value>.
504              
505             =head3 { max_value => $max_value }
506              
507             Makes sure the value is no more than C<$max_value>.
508              
509             =head3 { array => $boolean }
510              
511             If C<$boolean> is true, makes sure the value is actually an array reference.
512              
513             =head3 { hash => $boolean }
514              
515             If C<$boolean> is true, makes sure the value is actually a hash reference.
516              
517             =head3 { one_of => \@values }
518              
519             Makes sure a parameter's value is one of the provided acceptable values.
520              
521             =head3 { matches => $regex }
522              
523             Returns true if C<$value> matches the regular express (C) provided.
524             Will return false if C<$regex> is not a regular expression.
525              
526             =head3 { min_alpha => $integer }
527              
528             Returns a true value if C<$value> is a string that has at least C<$integer>
529             alphabetic (C and C) characters.
530              
531             =head3 { max_alpha => $integer }
532              
533             Returns a true value if C<$value> is a string that has at most C<$integer>
534             alphabetic (C and C) characters.
535              
536             =head3 { min_digits => $integer }
537              
538             Returns a true value if C<$value> is a string that has at least
539             C<$integer> digits (C<0-9>).
540              
541             =head3 { max_digits => $integer }
542              
543             Returns a true value if C<$value> is a string that has at most
544             C<$integer> digits (C<0-9>).
545              
546             =head3 { min_signs => $integer }
547              
548             Returns a true value if C<$value> has at least C<$integer> special or
549             sign characters (e.g. C<%^&!@#>, or basically anything that isn't C).
550              
551             =head3 { max_signs => $integer }
552              
553             Returns a true value if C<$value> has at most C<$integer> special or
554             sign characters (e.g. C<%^&!@#>, or basically anything that isn't C).
555              
556             =head3 { max_consec => $integer }
557              
558             Returns a true value if C<$value> does not have a sequence of consecutive
559             characters longer than C<$integer>. Consequtive characters are either
560             alphabetic (e.g. C) or numeric (e.g. C<1234>).
561              
562             =head3 { max_reps => $integer }
563              
564             Returns a true value if C<$value> does not contain a sequence of a repeated
565             character longer than C<$integer>. So, for example, if C<$integer> is 3,
566             then "aaa901" will return true (even though there's a repetition of the
567             'a' character it is not longer than three), while "9bbbb01" will return
568             false.
569              
570             =head2 ADVANCED FEATURES AND TIPS
571              
572             =head3 COMPLEX DATA STRUCTURES
573              
574             Brannigan can validate and process hash references of arbitrary complexity.
575             Input parameters may also be hash or array references.
576              
577             For arrays, the parameter needs to be marked with C<< array => 1 >>. The
578             validations and processing for the array's values are then provided as a hash
579             reference named C. For example:
580              
581             pictures => {
582             array => 1,
583             length_between => [1, 5],
584             values => {
585             min_length => 3,
586             validate => sub {
587             my $value = shift;
588             return $value =~ m!^http://! ? 1 : 0;
589             }
590             }
591             }
592              
593             In this example, "pictures" is an array parameter. When provided, the array must
594             contain between 1 and 5 items. Every item in the array must be a string of 3
595             characters or more, and must begin with the prefix "http://".
596              
597             For hashes, the parameter needs to be marked with C<< hash => 1 >>. The
598             validations and processing for the hash's attributes are then provided as a hash
599             reference named C. For example:
600              
601             name => {
602             hash => 1,
603             keys => {
604             first_name => {
605             length_between => [3, 10],
606             },
607             last_name => {
608             required => 1,
609             min_length => 3
610             }
611             }
612             }
613              
614             In this example, "name" is a hash paremeter. When provided, it must contain an
615             attribute called "first_name", which is an optional string between 3 or 10
616             characters long, and "last_name", which is a required string at least 3
617             characters longs.
618              
619             Array and hash parameters can also accept default values:
620              
621             complex_param => {
622             hash => 1,
623             keys => {
624             ...
625             },
626             default => { key1 => 'def1', key2 => 'def2' }
627             }
628              
629             Hash and arrays can fail validation in two ways: they can fail as a unit
630             (for example, schemas can enforce that an array will have between 2 and 5 items),
631             and specific items within them can fail (for example, schemas can enforce that
632             items in an array will be integers lower than 100).
633              
634             An array that failed as a unit will appear in the rejects hash-ref with its own
635             name. A specific array item or hash key that failed validation will appear with
636             dot notation:
637              
638             'name.first_name' => { length_between => [3, 10] },
639             'name.last_name' => { required => 1 },
640             'pictures' => { exact_length => 3 },
641             'numbers.1' => { max_value => 10 },
642              
643             In this example, specific keys failed in the "name" hash parameter. The "pictures"
644             array parameter failed as a unit (it should have exactly 3 items). The second
645             item in the "numbers" array parameter failed the "max_value" validator too.
646              
647             Brannigan's data structure support is infinitely recursive:
648              
649             pictures => {
650             array => 1,
651             values => {
652             hash => 1,
653             keys => {
654             filename => {
655             min_length => 5,
656             },
657             source => {
658             hash => 1,
659             keys => {
660             website => {
661             validate => sub { ... },
662             },
663             license => {
664             one_of => [qw/GPL FDL CC/],
665             },
666             },
667             },
668             },
669             },
670             }
671              
672             =head3 CROSS-SCHEMA CUSTOM VALIDATION METHODS
673              
674             Ad-hoc C functions are nice, but when you want to use the same custom
675             validation function in multiple places inside your schema (or in multiple
676             schemas), this can become unwieldy.
677              
678             Brannigan provides a simple mechanism to create custom, named validation
679             functions that can be used across schemas as if they were internal methods.
680              
681             This example creates a validation function called "forbid_words", which fails
682             string parameters that contain certain words:
683              
684             my $b = Brannigan->new();
685              
686             $b->register_validator('forbid_words', sub {
687             my ($value, @forbidden) = @_;
688             foreach (@forbidden) {
689             return 0 if $value =~ m/$_/;
690             }
691             return 1;
692             });
693              
694             $b->register_schema('user_input', {
695             params => {
696             text => {
697             required => 1,
698             forbid_words => ['curse_word', 'bad_word', 'ugly_word'],
699             }
700             }
701             });
702              
703             Note how the custom validation function accepts the value provided in the input,
704             and whatever was provided to 'forbid_words' in the configuration of the specific
705             parameter. In this case, the parameter called "text" forbids the words
706             "curse_word", "bad_word" and "ugly_word".
707              
708             If a parameter fails a named custom validation function, it will be added to the
709             rejects hash-ref like any other built-in validation function:
710              
711             text => [ 'forbid_words(curse_word, bad_word, ugly_word)' ]
712              
713             As an added bonus, you can use this mechanism to override Brannigan's built-in
714             validations. Just give the name of the validation method you wish to override,
715             along with the new code for this method.
716              
717             Note that you do not have to register a named validator before you register a
718             schema that uses it. You can register the schema first.
719              
720             =head3 REPEATING RULES FOR MULTIPLE PARAMETERS
721              
722             In previous versions, Brannigan allowed providing rules to multiple parameters
723             via regular expressions. This feature has been removed in version 2.0. Instead,
724             users can take advantage of the fact that schemas are simply Perl structures
725             and reuse rules via variables:
726              
727             my $date = { required => 1, matches => qr/^\d{4}-\d{2}-\d{2}$/ };
728              
729             my $schema = {
730             name => 'person',
731             params => {
732             birth_date => $date,
733             death_date => $date
734             }
735             };
736              
737             =head1 CONSTRUCTOR
738              
739             =head2 new( [ %options ] )
740              
741             Creates a new instance of Brannigan. Schemas must be registered separately
742             using the C method.
743              
744             Options:
745              
746             =over 1
747              
748             =item * C
749              
750             What to do with input parameters that are not defined in the processing schema.
751             Values: 'ignore' (default, keep unknown parameters as they are), 'remove' (delete
752             unknown parameters from the input), 'reject' (add to rejects and fail the
753             processing).
754              
755             =back
756              
757             =cut
758              
759             sub new {
760 107     107 1 1771193 my ( $class, $options ) = @_;
761              
762 107   100     515 $options ||= {};
763              
764             my $self = bless {
765             schemas => {},
766             validators => {},
767             merger => Hash::Merge->new('LEFT_PRECEDENT'),
768 107   100     467 handle_unknown => $options->{handle_unknown} || 'ignore',
769             _schema_cache => {}, # Cache for finalized schemas
770             }, $class;
771              
772             $self->register_validator(
773             'required',
774             sub {
775 170     170   335 my ( $value, $boolean ) = @_;
776              
777 170   100     859 return !$boolean || defined $value;
778             }
779 107         9360 );
780             $self->register_validator(
781             'is_true',
782             sub {
783 5     5   8 my ( $value, $boolean ) = @_;
784              
785 5   100     52 return !$boolean || $value;
786             }
787 107         434 );
788             $self->register_validator(
789             'length_between',
790             sub {
791 4     4   9 my ( $value, $min, $max ) = @_;
792              
793 4   100     11 return _length($value) >= $min && _length($value) <= $max;
794             }
795 107         381 );
796             $self->register_validator(
797             'min_length',
798             sub {
799 86     86   153 my ( $value, $min ) = @_;
800              
801 86         185 return _length($value) >= $min;
802             }
803 107         379 );
804             $self->register_validator(
805             'max_length',
806             sub {
807 14     14   32 my ( $value, $max ) = @_;
808              
809 14         26 return _length($value) <= $max;
810             }
811 107         392 );
812             $self->register_validator(
813             'exact_length',
814             sub {
815 6     6   17 my ( $value, $exlength ) = @_;
816              
817 6         14 return _length($value) == $exlength;
818             }
819 107         384 );
820             $self->register_validator(
821             'integer',
822             sub {
823 36     36   80 my ( $value, $boolean ) = @_;
824              
825 36   66     425 return !$boolean || $value =~ m/^\d+$/;
826             }
827 107         345 );
828             $self->register_validator(
829             'function',
830             sub {
831 0     0   0 my ( $value, $boolean ) = @_;
832              
833 0   0     0 return !$boolean || ref $value eq 'CODE';
834             }
835 107         356 );
836             $self->register_validator(
837             'value_between',
838             sub {
839 21     21   51 my ( $value, $min, $max ) = @_;
840              
841 21   100     182 return defined $value && $value >= $min && $value <= $max;
842             }
843 107         354 );
844             $self->register_validator(
845             'min_value',
846             sub {
847 10     10   24 my ( $value, $min ) = @_;
848              
849 10   66     66 return defined $value && $value >= $min;
850             }
851 107         339 );
852             $self->register_validator(
853             'max_value',
854             sub {
855 3     3   4 my ( $value, $max ) = @_;
856              
857 3   66     38 return defined $value && $value <= $max;
858             }
859 107         385 );
860             $self->register_validator(
861             'one_of',
862             sub {
863 10     10   20 my ( $value, @values ) = @_;
864              
865 10         13 foreach (@values) {
866 20 100       61 return 1 if $value eq $_;
867             }
868              
869 2         7 return;
870             }
871 107         381 );
872             $self->register_validator(
873             'matches',
874             sub {
875 20     20   41 my ( $value, $regex ) = @_;
876              
877 20   66     344 return ref $regex eq 'Regexp' && $value =~ $regex;
878             }
879 107         480 );
880             $self->register_validator(
881             'min_alpha',
882             sub {
883 3     3   5 my ( $value, $integer ) = @_;
884              
885 3         22 my @matches = ( $value =~ m/[A-Za-z]/g );
886              
887 3         19 return scalar @matches >= $integer;
888             }
889 107         472 );
890             $self->register_validator(
891             'max_alpha',
892             sub {
893 3     3   9 my ( $value, $integer ) = @_;
894              
895 3         41 my @matches = ( $value =~ m/[A-Za-z]/g );
896              
897 3         18 return scalar @matches <= $integer;
898             }
899 107         369 );
900             $self->register_validator(
901             'min_digits',
902             sub {
903 3     3   4 my ( $value, $integer ) = @_;
904              
905 3         19 my @matches = ( $value =~ m/[0-9]/g );
906              
907 3         12 return scalar @matches >= $integer;
908             }
909 107         399 );
910             $self->register_validator(
911             'max_digits',
912             sub {
913 3     3   3 my ( $value, $integer ) = @_;
914              
915 3         38 my @matches = ( $value =~ m/[0-9]/g );
916              
917 3         12 return scalar @matches <= $integer;
918             }
919 107         335 );
920             $self->register_validator(
921             'min_signs',
922             sub {
923 3     3   7 my ( $value, $integer ) = @_;
924              
925 3         25 my @matches = ( $value =~ m/[^A-Za-z0-9]/g );
926              
927 3         17 return scalar @matches >= $integer;
928             }
929 107         350 );
930             $self->register_validator(
931             'max_signs',
932             sub {
933 3     3   9 my ( $value, $integer ) = @_;
934              
935 3         28 my @matches = ( $value =~ m/[^A-Za-z0-9]/g );
936              
937 3         20 return scalar @matches <= $integer;
938             }
939 107         328 );
940             $self->register_validator(
941             'max_consec',
942             sub {
943 6     6   15 my ( $value, $integer ) = @_;
944              
945             # the idea here is to break the string into an array of characters,
946             # go over each character in the array starting at the first one,
947             # and making sure that character does not begin a sequence longer
948             # than allowed ($integer). This means we have recursive loops here,
949             # because for every character, we compare it to the following
950             # character and while they form a sequence, we move to the next pair
951             # and compare them until the sequence is broken. To make it a tad
952             # faster, our outer loop won't go over the entire characters array,
953             # but only up to the last character that might possibly form an
954             # invalid sequence. This character would be positioned $integer+1
955             # characters from the end.
956 6         28 my @chars = split( //, $value );
957 6         20 for ( my $i = 0 ; $i <= scalar(@chars) - $integer - 1 ; $i++ ) {
958 18         189 my $fc = $i; # first character for comparison
959 18         28 my $sc = $i + 1; # second character for comparison
960 18         23 my $sl = 1; # sequence length
961 18   100     93 while ( $sc <= $#chars
962             && ord( $chars[$sc] ) - ord( $chars[$fc] ) == 1 )
963             {
964             # characters are in sequence, increase counters
965             # and compare next pair
966 6         9 $sl++;
967 6         9 $fc++;
968 6         18 $sc++;
969             }
970 18 100       63 return if $sl > $integer;
971             }
972              
973 4         24 return 1;
974             }
975 107         500 );
976             $self->register_validator(
977             'max_reps',
978             sub {
979 6     6   12 my ( $value, $integer ) = @_;
980              
981             # The idea here is pretty much the same as in max_consec but we
982             # truely compare each pair of characters.
983              
984 6         27 my @chars = split( //, $value );
985 6         22 for ( my $i = 0 ; $i <= scalar(@chars) - $integer - 1 ; $i++ ) {
986 18         22 my $fc = $i; # First character for comparison
987 18         19 my $sc = $i + 1; # Second character for comparison
988 18         22 my $sl = 1; # Sequence length
989 18   100     60 while ( $sc <= $#chars && $chars[$sc] eq $chars[$fc] ) {
990              
991             # Characters are in sequence, increase counters
992             # and compare next pair
993 6         8 $sl++;
994 6         10 $fc++;
995 6         34 $sc++;
996             }
997 18 100       103 return if $sl > $integer;
998             }
999              
1000 4         25 return 1;
1001             }
1002 107         491 );
1003             $self->register_validator(
1004             'array',
1005             sub {
1006 40     40   75 my ( $value, $boolean ) = @_;
1007              
1008 40 100       188 $boolean
    0          
    50          
1009             ? ref $value eq 'ARRAY'
1010             ? 1
1011             : return
1012             : ref $value eq 'ARRAY' ? return
1013             : 1;
1014             }
1015 107         391 );
1016             $self->register_validator(
1017             'hash',
1018             sub {
1019 46     46   77 my ( $value, $boolean ) = @_;
1020              
1021 46 100       230 $boolean
    0          
    50          
1022             ? ref $value eq 'HASH'
1023             ? 1
1024             : return
1025             : ref $value eq 'HASH' ? return
1026             : 1;
1027             }
1028 107         666 );
1029              
1030             # Inject the schema validator schema used to validate user schemas. This is
1031             # a Brannigan validation schema itself!
1032             # TODO: figure out the best way to validate parameter definitions, as they
1033             # are named by the user, can contain validators we don't know yet, and other
1034             # complications.
1035             $self->{schemas}->{'__brannigan_schema_validator__'} = {
1036             name => { required => 1, min_length => 1 },
1037             schema => {
1038             params => {
1039             required => 1,
1040             hash => 1,
1041             },
1042             inherits_from => {
1043             array => 1,
1044             preprocess => sub {
1045 0     0   0 my $value = shift;
1046              
1047             # Convert single string to array for uniform processing
1048 0 0       0 return ref $value eq 'ARRAY' ? $value : [$value]
    0          
1049             if defined $value;
1050 0         0 return $value;
1051             }
1052             },
1053 107         1315 postprocess => {
1054             function => 1,
1055             }
1056             }
1057             };
1058              
1059 107         1503 return $self;
1060             }
1061              
1062             =head1 OBJECT METHODS
1063              
1064             =head2 register_schema( $name, \%schema )
1065              
1066             Registers a validation schema with the given name. If a schema with the same
1067             name already exists, it will be overridden. The schema hash-ref should not
1068             contain a C key as it's provided separately. Returns the C
1069             object itself for chain-ability.
1070              
1071             =cut
1072              
1073             sub register_schema {
1074 145     145 1 53348 my ( $self, $name, $schema ) = @_;
1075              
1076 145 50 33     637 die "Schema name is required" unless defined $name && length $name;
1077 145 50       388 die "Schema must be a hash reference" unless ref $schema eq 'HASH';
1078              
1079             # Validate the schema structure before storing it
1080 145         473 $self->_validate_schema_definition( $name, $schema );
1081              
1082             # Store the schema with the provided name
1083 145         330 $self->{schemas}->{$name} = $schema;
1084              
1085             # Clear the schema cache since we have a new/updated schema
1086 145         398 $self->{_schema_cache} = {};
1087              
1088 145         300 return $self;
1089             }
1090              
1091             =head2 register_validator( $name, $code )
1092              
1093             Registers a new named validator function. C<$code> is a reference to a subroutine
1094             that receives a value as a parameter and returns a boolean value indicating
1095             whether the value is valid or not. The method can be used to override
1096             built-in validation functions.
1097              
1098             =cut
1099              
1100             sub register_validator {
1101 2470     2470 1 11353 my ( $self, $name, $code ) = @_;
1102 2470 50 33     8713 return unless $name && $code && ref $code eq 'CODE';
      33        
1103 2470         5383 $self->{validators}->{$name} = $code;
1104             }
1105              
1106             =head2 handle_unknown( [$value] )
1107              
1108             Gets or sets the behavior for handling unknown input parameters.
1109             Accepted values: 'ignore', 'remove', 'reject'.
1110              
1111             =cut
1112              
1113             sub handle_unknown {
1114 5     5 1 33 my ( $self, $value ) = @_;
1115              
1116 5 100       11 if ( defined $value ) {
1117 3 100       23 die "Invalid handle_unknown value: $value"
1118             unless $value =~ /^(ignore|remove|reject)$/;
1119 2         3 $self->{handle_unknown} = $value;
1120 2         28 return $self;
1121             }
1122              
1123 2         9 return $self->{handle_unknown};
1124             }
1125              
1126             =head2 process( $schema, \%params )
1127              
1128             Receives the name of a schema and a hash reference of input parameters.
1129             Performs pre-processing, validation and post-processing as described in the
1130             manual.
1131              
1132             Any processing that modifies the input is performed in-place.
1133              
1134             Returns an undefined value if there were no rejects. Returns a hash reference
1135             of rejects if there were any.
1136              
1137             =head1 FUNCTIONAL INTERFACE
1138              
1139             =head2 process( \%schema, \%params )
1140              
1141             Accepts a schema hash-ref and an input hash-ref, and performs pre-processing,
1142             validation and post-processing. If no parameters failed validation, an undefined
1143             value is returned. Otherwise a hash reference of rejects is returned.
1144              
1145             Note that this interface does not allow for custom validation functions and
1146             schema inheritance. You are not required to give the schema a name when using
1147             this interface.
1148              
1149             my $rejects = Brannigan::process( $schema, $params );
1150              
1151             =cut
1152              
1153             sub process {
1154              
1155             # Called as a method
1156 392 100 66 392 1 315261 if ( scalar @_ == 3 && ref $_[0] eq __PACKAGE__ ) {
1157 306         692 my ( $self, $schema_name, $params ) = @_;
1158              
1159             # Finalize the schema, merging it with any inherited schemas
1160 306         665 my $schema = $self->_finalize_schema($schema_name);
1161              
1162             # Execute preprocessing on input parameters
1163 306         762 $self->_preprocess( $schema, $params );
1164              
1165             # Validate input parameters
1166 306         771 my $rejects = $self->_validate( $params, $schema->{params} );
1167 306 100       592 if ($rejects) {
1168 60         3255 return $rejects;
1169             }
1170              
1171             # Execute postprocessing on input parameters
1172 246         718 $self->_postprocess( $schema, $params );
1173              
1174 246         5221 return;
1175             }
1176              
1177             # Called as a function
1178 86         166 my ( $schema, $params ) = @_;
1179 86         348 my $b = Brannigan->new();
1180 86         193 $b->register_schema( 'temp', $schema );
1181 86         154 return $b->process( 'temp', $params );
1182             }
1183              
1184             ############################
1185             ##### INTERNAL METHODS #####
1186             ############################
1187              
1188             # _length( $value )
1189             # ------------------------------------------------------------------------
1190             # Returns the length of a string value in characters, or an array value in
1191             # items.
1192              
1193             sub _length {
1194 113 100   113   558 return ref $_[0] eq 'ARRAY' ? scalar( @{ $_[0] } ) : length( $_[0] );
  20         160  
1195             }
1196              
1197             # _finalize_schema( $schema_name )
1198             # --------------------------------------------------------------------------
1199             # Builds the final "tree" of validations and parsing methods to be performed
1200             # on the parameter hash-ref during processing.
1201              
1202             sub _finalize_schema {
1203 317     317   557 my ( $self, $schema_name ) = @_;
1204              
1205             # Check cache first
1206             return $self->{_schema_cache}->{$schema_name}
1207 317 100       689 if exists $self->{_schema_cache}->{$schema_name};
1208              
1209 293   50     746 my $schema = $self->{schemas}->{$schema_name}
1210             || die "Unknown schema $schema_name";
1211              
1212             # get a list of all schemas to inherit from
1213 293 100       573 if ( $schema->{inherits_from} ) {
1214             my @inherited_schemas =
1215             $schema->{inherits_from}
1216             && ref $schema->{inherits_from} eq 'ARRAY'
1217 1         5 ? @{ $schema->{inherits_from} }
1218             : $schema->{inherits_from} ? ( $schema->{inherits_from} )
1219 10 50 66     75 : ();
    100          
1220              
1221 10         24 foreach my $inherited_schema_name (@inherited_schemas) {
1222 11   50     197 my $inherited_schema = $self->{schemas}->{$inherited_schema_name}
1223             || next;
1224              
1225             # Recursively finalize inherited schemas to handle deep inheritance
1226 11         63 $inherited_schema = $self->_finalize_schema($inherited_schema_name);
1227 11         52 $schema = $self->{merger}->merge( $schema, $inherited_schema );
1228             }
1229             }
1230              
1231             # Cache the finalized schema for future use
1232 293         3114 $self->{_schema_cache}->{$schema_name} = $schema;
1233              
1234 293         537 return $schema;
1235             }
1236              
1237             # _validate( \%params, %rules )
1238             # ------------------------------------------------
1239             # Validates the hash-ref of input parameters against a finalized schema, returns
1240             # undef if there are no rejects or a hash-ref of rejects if there are any.
1241              
1242             sub _validate {
1243 306     306   483 my ( $self, $params, $rules ) = @_;
1244              
1245 306         469 my $rejects = {};
1246              
1247             # Handle unknown parameters according to the object's configuration
1248 306         749 $self->_handle_unknown_params( $params, $rules, $rejects );
1249              
1250             # Go over all the parameters in the schema and validate them
1251 306         823 foreach my $param ( sort keys %{$rules} ) {
  306         714  
1252             $self->_validate_param( $param, $params->{$param},
1253 282         1046 $rules->{$param}, $rejects );
1254             }
1255              
1256 306 100       917 return $rejects if scalar keys %$rejects;
1257 246         593 return;
1258             }
1259              
1260             # _validate_param( $value, \%rules )
1261             # ------------------------------------------------
1262             # Receives a parameter value and a hash-ref of validation rules to assert.
1263             # Returns a list of validations that failed for this parameter, if any.
1264              
1265             sub _validate_param {
1266 430     430   808 my ( $self, $name, $value, $rules, $rejects ) = @_;
1267              
1268             # is this a scalar, array or hash parameter?
1269 430 100       956 if ( $rules->{hash} ) {
    100          
1270 46         124 $self->_validate_hash( $name, $value, $rules, $rejects );
1271             } elsif ( $rules->{array} ) {
1272 40         106 $self->_validate_array( $name, $value, $rules, $rejects );
1273             } else {
1274 344         690 $self->_validate_scalar( $name, $value, $rules, $rejects );
1275             }
1276             }
1277              
1278             # _validate_scalar( $value, \%rules, [$type] )
1279             # ----------------------------------------------------------
1280             # Receives the name of a parameter, its value, and a hash-ref of validations
1281             # to assert against. Returns a list of all failed validations for this
1282             # parameter. If the parameter is a child of a hash/array parameter, then
1283             # C<$type> must be provided with either 'hash' or 'array'.
1284              
1285             sub _validate_scalar {
1286 430     430   751 my ( $self, $name, $value, $rules, $rejects ) = @_;
1287              
1288 430         527 my $is_required = 0;
1289              
1290 430         568 foreach my $v (
1291             sort {
1292 427 100       896 return -1 if $a eq 'required'; # $a is 'required' → it comes first
1293 328 100       577 return 1 if $b eq 'required'; # $b is 'required' → it comes first
1294             return
1295 245         599 lc($a) cmp lc($b)
1296             ; # otherwise sort alphabetically, case-insensitive
1297 430         1260 } keys %{$rules}
1298             )
1299             {
1300             next
1301 757 100 100     4705 if $v eq 'postprocess'
      100        
      100        
      100        
1302             || $v eq 'preprocess'
1303             || $v eq 'default'
1304             || $v eq 'values'
1305             || $v eq 'keys';
1306              
1307 552 100 100     1339 $is_required = 1 if $v eq 'required' && $rules->{$v};
1308              
1309 552 100 100     1380 last if !$is_required && !defined $value;
1310              
1311             # Get the arguments we're passing to the validation function
1312             my @args =
1313             ref $rules->{$v} eq 'ARRAY'
1314 40         126 ? @{ $rules->{$v} }
1315 540 100       1316 : ( $rules->{$v} );
1316              
1317             my $validator_func =
1318 540 100       1204 $v eq 'validate' ? $rules->{$v} : $self->{validators}->{$v};
1319              
1320 540 100       1066 if ( !$validator_func->( $value, @args ) ) {
1321 70 100       508 if ( $v eq 'validate' ) {
1322 4         12 $rejects->{$name}->{$v} = 1;
1323             } else {
1324 66 100       271 $rejects->{$name}->{$v} = scalar @args > 1 ? \@args : $args[0];
1325             }
1326              
1327             # Do not bother with other validation functions if the 'required'
1328             # validator failed (i.e. parameter was not provided at all).
1329 70 100 66     321 if ( $v eq 'required' && $is_required ) {
1330 5         60 last;
1331             }
1332             }
1333             }
1334             }
1335              
1336             # _validate_array( $value, \%rules )
1337             # ------------------------------------------------
1338             # Receives a parameter value and a hash-ref of validation rules to assert.
1339             # Returns a hash-ref of rejects for the value, if any, otherwise returns undef.
1340              
1341             sub _validate_array {
1342 40     40   103 my ( $self, $name, $value, $rules, $rejects ) = @_;
1343              
1344             # Invoke validations on the array itself
1345 40         153 $self->_validate_scalar( $name, $value, $rules, $rejects );
1346 40 100       102 return if exists $rejects->{$name};
1347              
1348             # Invoke validations on the items of the array value
1349 36 100       100 if ( $rules->{values} ) {
1350 28         40 my $i = 0;
1351 28         58 foreach (@$value) {
1352             $self->_validate_param( "$name.$i", $_, $rules->{values},
1353 45         187 $rejects );
1354 45         211 $i++;
1355             }
1356             }
1357             }
1358              
1359             # _validate_hash( $value, \%rules )
1360             # -----------------------------------------------
1361             # Receives a parameter value and a hash-ref of rules to assert.
1362             # Returns a hash-ref of rejects for the value, if any, or an undefined value
1363             # otherwise.
1364              
1365             sub _validate_hash {
1366 46     46   77 my ( $self, $name, $value, $rules, $rejects ) = @_;
1367              
1368             # Invoke validations on the parameter value itself
1369 46         122 $self->_validate_scalar( $name, $value, $rules, $rejects );
1370 46 100       119 return if exists $rejects->{$name};
1371              
1372             # Handle unknown keys in nested hash if rules are defined
1373 44 100 100     185 if ( $rules->{keys} && $self->{handle_unknown} ne 'ignore' ) {
1374             $self->_handle_unknown_nested_params( $name, $value, $rules->{keys},
1375 7         17 $rejects );
1376             }
1377              
1378             # Invoke validations on the key-value pairs of the hash
1379 44 100       86 if ( $rules->{keys} ) {
1380 40         45 foreach my $key ( keys %{ $rules->{keys} } ) {
  40         107  
1381             $self->_validate_param( "$name.$key", $value->{$key},
1382 103         352 $rules->{keys}->{$key}, $rejects );
1383             }
1384             }
1385             }
1386              
1387             # _preprocess( \%schema, \%params )
1388             # -------------------------------------------------
1389             # Receives a finalized schema and a hash-ref of parameter values, and performs
1390             # preprocessing.
1391              
1392             sub _preprocess {
1393 306     306   481 my ( $self, $schema, $params ) = @_;
1394              
1395 306         352 foreach my $param ( sort keys %{ $schema->{params} } ) {
  306         1108  
1396             $self->_preprocess_param( $param, $params,
1397 282         638 $schema->{params}->{$param} );
1398             }
1399             }
1400              
1401             # _preprocess_param( $name, \%params, \%rules )
1402             # -----------------------------------------------
1403             # Recursively preprocesses a parameter, applying defaults and preprocess
1404             # functions at all nesting levels (top-level, hashes, and array items).
1405              
1406             sub _preprocess_param {
1407 385     385   695 my ( $self, $name, $params, $rules ) = @_;
1408              
1409             # Early exit if no preprocessing needed
1410             return
1411             unless ( defined $rules->{default} && !defined $params->{$name} )
1412             || ( defined $rules->{preprocess} && defined $params->{$name} )
1413             || ( $rules->{hash} && $rules->{keys} )
1414 385 100 100     3055 || ( $rules->{array} && $rules->{values} );
      66        
      100        
      100        
      100        
      100        
      100        
1415              
1416             # Apply default value if parameter not provided
1417 131 100 100     355 if ( defined $rules->{default} && !defined $params->{$name} ) {
1418             $params->{$name} =
1419             ref $rules->{default} eq 'CODE'
1420             ? $rules->{default}->()
1421 78 100       229 : $rules->{default};
1422             }
1423              
1424             # Apply preprocess function if parameter exists and has preprocess
1425 131 100 66     349 if ( defined $rules->{preprocess} && defined $params->{$name} ) {
1426 16         38 $params->{$name} = $rules->{preprocess}->( $params->{$name} );
1427             }
1428              
1429             # Recursively preprocess nested structures
1430 131 50       474 if ( defined $params->{$name} ) {
1431 131 100 100     710 if ( $rules->{hash}
    100 66        
      100        
      66        
1432             && $rules->{keys}
1433             && ref( $params->{$name} ) eq 'HASH' )
1434             {
1435             # Preprocess hash keys recursively
1436 13         25 foreach my $key ( keys %{ $rules->{keys} } ) {
  13         35  
1437             $self->_preprocess_param( $key, $params->{$name},
1438 29         85 $rules->{keys}->{$key} );
1439             }
1440             } elsif ( $rules->{array}
1441             && $rules->{values}
1442             && ref( $params->{$name} ) eq 'ARRAY' )
1443             {
1444             # Preprocess array items recursively
1445 29         42 for my $i ( 0 .. $#{ $params->{$name} } ) {
  29         111  
1446 46 50 66     295 if ( $rules->{values}->{hash}
      33        
1447             && $rules->{values}->{keys}
1448             && ref( $params->{$name}->[$i] ) eq 'HASH' )
1449             {
1450             # Each array item is a hash - preprocess its keys
1451 27         35 foreach my $key ( keys %{ $rules->{values}->{keys} } ) {
  27         77  
1452             $self->_preprocess_param(
1453             $key,
1454             $params->{$name}->[$i],
1455 74         183 $rules->{values}->{keys}->{$key}
1456             );
1457             }
1458             }
1459              
1460             # Note: We could extend this to handle other array item types,
1461             # but hash items are the most common use case
1462             }
1463             }
1464             }
1465             }
1466              
1467             # _postprocess( \%schema, \%params )
1468             # -------------------------------------------------
1469             # Receives a finalized schema and a hash-ref of parameter values, and performs
1470             # postprocessing.
1471              
1472             sub _postprocess {
1473 246     246   400 my ( $self, $schema, $params ) = @_;
1474              
1475 246         389 foreach my $param ( sort keys %{ $schema->{params} } ) {
  246         572  
1476 183 100       462 next if !defined $schema->{params}->{$param}->{postprocess};
1477              
1478             # This is a direct rule
1479             $params->{$param} =
1480 7         34 $schema->{params}->{$param}->{postprocess}->( $params->{$param} );
1481             }
1482              
1483 246 100 66     687 if ( $schema->{postprocess} && ref $schema->{postprocess} eq 'CODE' ) {
1484 7         23 $schema->{postprocess}->($params);
1485             }
1486             }
1487              
1488             # _handle_unknown_params( \%params, \%rules, \%rejects )
1489             # ------------------------------------------------
1490             # Handles input parameters that are not defined in the schema according
1491             # to the object's handle_unknown setting.
1492              
1493             sub _handle_unknown_params {
1494 306     306   521 my ( $self, $params, $rules, $rejects ) = @_;
1495              
1496 306 100       760 return if $self->{handle_unknown} eq 'ignore';
1497              
1498             # Find parameters in input that are not in the schema
1499 9         14 my @unknown_params;
1500 9         41 foreach my $param ( keys %$params ) {
1501 26 100       87 push @unknown_params, $param unless exists $rules->{$param};
1502             }
1503              
1504 9 100       37 if ( $self->{handle_unknown} eq 'remove' ) {
    50          
1505              
1506             # Remove unknown parameters from input
1507 4         16 delete $params->{$_} for @unknown_params;
1508             } elsif ( $self->{handle_unknown} eq 'reject' ) {
1509              
1510             # Add unknown parameters to rejects
1511 5         24 $rejects->{$_} = { unknown => 1 } for @unknown_params;
1512             }
1513             }
1514              
1515             # _handle_unknown_nested_params( $path, \%value, \%expected_keys, \%rejects )
1516             # -------------------------------------------------------------------------
1517             # Handles unknown parameters in nested hash structures according to the
1518             # object's handle_unknown setting. Similar to _handle_unknown_params but
1519             # works with nested paths and hash values.
1520              
1521             sub _handle_unknown_nested_params {
1522 7     7   13 my ( $self, $path, $value, $expected_keys, $rejects ) = @_;
1523              
1524 7 50       15 return if $self->{handle_unknown} eq 'ignore';
1525 7 50       13 return unless ref($value) eq 'HASH';
1526              
1527             # Find keys in the hash that are not in the expected keys
1528 7         9 my @unknown_keys;
1529 7         25 foreach my $key ( keys %$value ) {
1530 21 100       65 push @unknown_keys, $key unless exists $expected_keys->{$key};
1531             }
1532              
1533 7 100       33 if ( $self->{handle_unknown} eq 'remove' ) {
    50          
1534              
1535             # Remove unknown keys from the nested hash
1536 2         7 delete $value->{$_} for @unknown_keys;
1537             } elsif ( $self->{handle_unknown} eq 'reject' ) {
1538              
1539             # Add unknown keys to rejects with nested path notation
1540 5         7 for my $key (@unknown_keys) {
1541 5 50       14 my $nested_path = $path ? "$path.$key" : $key;
1542 5         18 $rejects->{$nested_path} = { unknown => 1 };
1543             }
1544             }
1545             }
1546              
1547             # _validate_schema_definition( $name, \%schema )
1548             # -----------------------------------------------
1549             # Validates a schema definition for common errors before registration.
1550             # Dies with descriptive error messages if the schema is invalid.
1551              
1552             sub _validate_schema_definition {
1553 145     145   284 my ( $self, $name, $schema ) = @_;
1554              
1555             # Use Brannigan itself to validate schema definitions.
1556              
1557             # Skip validation for the schema validator schema itself
1558 145 50       374 return if $name eq '__brannigan_schema_validator__';
1559              
1560 145         250 my $handle_unknown = $self->{handle_unknown};
1561 145         271 $self->{handle_unknown} = 'ignore';
1562 145         549 my $rejects = $self->process(
1563             '__brannigan_schema_validator__',
1564             {
1565             name => $name,
1566             schema => $schema
1567             }
1568             );
1569 145         396 $self->{handle_unknown} = $handle_unknown;
1570              
1571 145 50       385 die "Schema validation failed" if $rejects;
1572             }
1573              
1574             =head1 UPGRADING FROM 1.x TO 2.0
1575              
1576             Version 2.0 of Brannigan includes significant breaking changes. This guide will
1577             help you upgrade your existing code.
1578              
1579             =head2 BREAKING CHANGES
1580              
1581             =head3 Constructor and Schema Registration
1582              
1583             B
1584              
1585             my $b = Brannigan->new(
1586             { name => 'schema1', params => { ... } },
1587             { name => 'schema2', params => { ... } }
1588             );
1589              
1590             B
1591              
1592             my $b = Brannigan->new(); # No schemas in constructor
1593             $b->register_schema('schema_name', { params => { ... } });
1594             $b->register_schema('another_schema', { params => { ... } });
1595              
1596             =head3 Method Names
1597              
1598             Several methods have been renamed for clarity:
1599              
1600             =over 2
1601              
1602             =item * C => C
1603              
1604             =item * C => C
1605              
1606             =back
1607              
1608             =head3 Return Value Changes
1609              
1610             The C method now returns different values:
1611              
1612             B Always returned a hash-ref with processed parameters and optional
1613             C<_rejects> key.
1614              
1615             B Returns C on success, hash-ref of rejects on failure.
1616             Processing happens in-place on the input hash-ref.
1617              
1618             # Old style
1619             my $result = $b->process('schema', \%params);
1620             if ($result->{_rejects}) {
1621             # Handle errors
1622             }
1623              
1624             # New style
1625             my $rejects = $b->process('schema', \%params);
1626             if ($rejects) {
1627             # Handle $rejects hash-ref directly
1628             }
1629             # %params is modified in-place with processed values
1630              
1631             =head3 Error Structure Changes
1632              
1633             The structure of validation errors has changed significantly:
1634              
1635             B
1636              
1637             {
1638             _rejects => {
1639             parameter => ['required(1)', 'min_length(5)'],
1640             nested => { path => { param => ['max_value(100)'] } }
1641             }
1642             }
1643              
1644             B
1645              
1646             {
1647             'parameter' => { required => 1, min_length => 5 },
1648             'nested.path.param' => { max_value => 100 }
1649             }
1650              
1651             Key changes:
1652              
1653             =over 4
1654              
1655             =item * Error paths are flattened using dot notation
1656              
1657             =item * Validator names and arguments are returned as key-value pairs
1658              
1659             =item * No more C<_rejects> wrapper
1660              
1661             =item * Unknown parameters are reported with C<< { unknown => 1 } >>
1662              
1663             =back
1664              
1665             =head3 Processing Function Changes
1666              
1667             =over 3
1668              
1669             =item * C functions → C functions
1670              
1671             =item * Default values are now calculated B validation (they can fail validation)
1672              
1673             =item * C functions must return a replacement value, not a hash-ref
1674              
1675             =back
1676              
1677             B
1678              
1679             parse => sub {
1680             my $value = shift;
1681             return { parameter_name => process($value) };
1682             }
1683              
1684             B
1685              
1686             postprocess => sub {
1687             my $value = shift;
1688             return process($value); # Return the processed value directly
1689             }
1690              
1691             =head2 NEW FEATURES
1692              
1693             =head3 Preprocessing
1694              
1695             You can now preprocess input before validation:
1696              
1697             params => {
1698             username => {
1699             preprocess => sub { lc }, # Lowercase parameter value
1700             required => 1,
1701             min_length => 3
1702             }
1703             }
1704              
1705             =head3 Global Postprocessing
1706              
1707             Add a global postprocess function to your schema:
1708              
1709             {
1710             params => { ... },
1711             postprocess => sub {
1712             my $params = shift;
1713             $params->{computed_field} = calculate($params);
1714             # Modify $params in-place, no return value needed
1715             }
1716             }
1717              
1718             =head3 Unknown Parameter Handling
1719              
1720             Control how unknown parameters are handled:
1721              
1722             my $b = Brannigan->new();
1723             $b->handle_unknown('ignore'); # Default: preserve unknown params
1724             $b->handle_unknown('remove'); # Remove unknown params
1725             $b->handle_unknown('reject'); # Fail validation on unknown params
1726              
1727             This works at all nesting levels (top-level, nested hashes, array items).
1728              
1729             =head3 Enhanced Default Values
1730              
1731             Default values now work in nested structures:
1732              
1733             params => {
1734             users => {
1735             array => 1,
1736             values => {
1737             hash => 1,
1738             keys => {
1739             name => { required => 1 },
1740             role => { default => 'user' }, # Applied to each array item
1741             active => { default => 1 }
1742             }
1743             }
1744             }
1745             }
1746              
1747             =head3 Improved Schema Inheritance
1748              
1749             Schema inheritance now works recursively and merges parameter definitions:
1750              
1751             $b->register_schema('base', {
1752             params => {
1753             name => { required => 1, max_length => 50 }
1754             }
1755             });
1756              
1757             $b->register_schema('extended', {
1758             inherits => 'base',
1759             params => {
1760             name => { min_length => 2 }, # Merges with base constraints
1761             email => { required => 1 } # Additional parameter
1762             }
1763             });
1764              
1765             =head2 REMOVED FEATURES
1766              
1767             The following features have been removed:
1768              
1769             =over
1770              
1771             =item * Parameter groups (use global C instead)
1772              
1773             =item * Regular expression parameter definitions
1774              
1775             =item * Scope-local "_all" validators
1776              
1777             =item * C validator
1778              
1779             =item * C validator (use C<< handle_unknown => 'reject' >> instead)
1780              
1781             =item * C schema option (use C instead)
1782              
1783             =back
1784              
1785             =head1 AUTHOR
1786              
1787             Ido Perlmuter, C<< >>
1788              
1789             =head1 BUGS
1790              
1791             Please report any bugs or feature requests to L.
1792              
1793             =head1 SUPPORT
1794              
1795             You can find documentation for this module with the perldoc command.
1796              
1797             perldoc Brannigan
1798              
1799             =head1 ACKNOWLEDGEMENTS
1800              
1801             Brannigan was inspired by L (Al Newkirk) and the "Ketchup" jQuery
1802             validation plugin (L).
1803              
1804             =head1 LICENSE AND COPYRIGHT
1805              
1806             Copyright 2025 Ido Perlmuter
1807              
1808             Licensed under the Apache License, Version 2.0 (the "License");
1809             you may not use this file except in compliance with the License.
1810             You may obtain a copy of the License at
1811              
1812             http://www.apache.org/licenses/LICENSE-2.0
1813              
1814             Unless required by applicable law or agreed to in writing, software
1815             distributed under the License is distributed on an "AS IS" BASIS,
1816             WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1817             See the License for the specific language governing permissions and
1818             limitations under the License.
1819              
1820             =cut
1821              
1822             1;
1823             __END__