The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Brannigan - Comprehensive, flexible system for validating and parsing input, mainly targeted at web applications.

SYNOPSIS

    use Brannigan;

    my %scheme1 = ( name => 'scheme1', params => ... );
    my %scheme2 = ( name => 'scheme2', params => ... );
    my %scheme3 = ( name => 'scheme3', params => ... );

    # use the OO interface
    my $b = Brannigan->new(\%scheme1, \%scheme2);
    $b->add_scheme(\%scheme3);

    my $parsed = $b->process('scheme1', \%params);
    if ($parsed->{_rejects}) {
            die $parsed->{_rejects};
    } else {
            return $parsed;
    }

    # Or use the functional interface
    my $parsed = Brannigan::process(\%scheme1, \%params);
    if ($parsed->{_rejects}) {
            die $parsed->{_rejects};
    } else {
            return $parsed;
    }

For a more comprehensive example, see "MANUAL" in this document or the Brannigan::Examples document.

DESCRIPTION

Brannigan is an attempt to ease the pain of collecting, validating and parsing input parameters in web applications. It's designed to answer both of the main problems that web applications face:

Brannigan's approach to data validation is as follows: define a structure of parameters and their needed validations, and let the module automatically examine input parameters against this structure. Brannigan provides you with common validation methods that are used everywhere, and also allows you to create custom validations easily. This structure also defines how, if at all, the input should be parsed. This is akin to schema-based validations such as XSD, but much more functional, and most of all flexible.

Check the next section for an example of such a structure. I call this structure a validation/parsing scheme. Schemes can inherit all the properties of other schemes, which allows you to be much more flexible in certain situations. Imagine you have a blogging application. A base scheme might define all validations and parsing needed to create a new blog post from a user's input. When editing a post, however, some parameters that were required when creating the post might not be required now (so you can just use older values), and maybe new parameters are introduced. Inheritance helps you avoid repeating yourself. You can another scheme which gets all the properties of the base scheme, only changing whatever it is needs changing (and possibly adding specific properties that don't exist in the base scheme).

MANUAL

In the following manual, we will look at the following example. It is based on Catalyst, but should be fairly understandable for non-Catalyst users. Do not be alarmed by the size of this, this is only because it displays basically every aspect of Brannigan.

This example uses Catalyst, but should be pretty self explanatory. It's fairly complex, since it details pretty much all of the available Brannigan functionality, so don't be alarmed by the size of this thing.

    package MyApp::Controller::Post;

    use strict;
    use warnings;
    use Brannigan;

    # create a new Brannigan object with two validation/parsing schemes:
    my $b = Brannigan->new({
            name => 'post',
            ignore_missing => 1,
            params => {
                    subject => {
                            required => 1,
                            length_between => [3, 40],
                    },
                    text => {
                            required => 1,
                            min_length => 10,
                            validate => sub {
                                    my $value = shift;

                                    return undef unless $value;

                                    return $value =~ m/^lorem ipsum/ ? 1 : undef;
                            }
                    },
                    day => {
                            required => 0,
                            integer => 1,
                            value_between => [1, 31],
                    },
                    mon => {
                            required => 0,
                            integer => 1,
                            value_between => [1, 12],
                    },
                    year => {
                            required => 0,
                            integer => 1,
                            value_between => [1900, 2900],
                    },
                    section => {
                            required => 1,
                            integer => 1,
                            value_between => [1, 3],
                            parse => sub {
                                    my $val = shift;

                                    my $ret = $val == 1 ? 'reviews' :
                                              $val == 2 ? 'receips' :
                                              'general';

                                    return { section => $ret };
                            },
                    },
                    id => {
                            required => 1,
                            exact_length => 10,
                            value_between => [1000000000, 2000000000],
                    },
                    '/^picture_(\d+)$/' => {
                            length_between => [3, 100],
                            validate => sub {
                                    my ($value, $num) = @_;

                                    ...
                            },
                    },
                    picture_1 => {
                            default => 'http://www.example.com/avatar.png',
                    },
                    array_of_ints => {
                            array => 1,
                            min_length => 3,
                            values => {
                                    integer => 1,
                            },
                    },
                    hash_of_langs => {
                            hash => 1,
                            keys => {
                                    _all => {
                                            exact_length => 10,
                                    },
                                    en => {
                                            required => 1,
                                    },
                            },
                    },
            },
            groups => {
                    date => {
                            params => [qw/year mon day/],
                            parse => sub {
                                    my ($year, $mon, $day) = @_;
                                    return undef unless $year && $mon && $day;
                                    return { date => $year.'-'.$mon.'-'.$day };
                            },
                    },
                    tags => {
                            regex => '/^tags_(en|he|fr)$/',
                            forbid_words => ['bad_word', 'very_bad_word'],
                            parse => sub {
                                    return { tags => \@_ };
                            },
                    },
            },
    }, {
            name => 'edit_post',
            inherits_from => 'post',
            params => {
                    subject => {
                            required => 0, # subject is no longer required
                    },
                    id => {
                            forbidden => 1,
                    },
            },
    });

    # create the custom 'forbid_words' validation method
    $b->custom_validation('forbid_words', sub {
            my $value = shift;

            foreach (@_) {
                    return 0 if $value =~ m/$_/;
            }

            return 1;
    });

    # post a new blog post
    sub new_post : Local {
            my ($self, $c) = @_;

            # get input parameters hash-ref
            my $params = $c->request->params;

            # process the parameters
            my $parsed_params = $b->process('post', $params);

            if ($parsed_params->{_rejects}) {
                    die $c->list_errors($parsed_params);
            } else {
                    $c->model('DB::BlogPost')->create($parsed_params);
            }
    }

    # edit a blog post
    sub edit_post : Local {
            my ($self, $c, $id) = @_;

            my $params = $b->process('edit_posts', $c->req->params);

            if ($params->{_rejects}) {
                    die $c->list_errors($params);
            } else {
                    $c->model('DB::BlogPosts')->find($id)->update($params);
            }
    }

HOW BRANNIGAN WORKS

In essence, Brannigan works in three stages (which all boil down to one single command):

HOW SCHEMES LOOK

The validation/parsing scheme defines the structure of the data you're expecting to receive, along with information about the way it should be validated and parsed. Schemes are created by passing them to the Brannigan constructor. You can pass as many schemes as you like, and these schemes can inherit from one another. You can create the Brannigan object that gets these schemes wherever you want. Maybe in a controller of your web app that will directly use this object to validate and parse input it gets, or maybe in a special validation class that will hold all schemes. It doesn't matter where, as long as you make the object available for your application.

A scheme is a hash-ref based data structure that has the following keys:

BUILT-IN VALIDATION METHODS

As mentioned earlier, Brannigan comes with a set of built-in validation methods which are most common and useful everywhere. For a list of all validation methods provided by Brannigan, check Brannigan::Validations.

CROSS-SCHEME CUSTOM VALIDATION METHODS

Custom validate methods are nice, but when you want to use the same custom validation method in different places inside your scheme, or more likely in different schemes altogether, repeating the definition of each custom method in every place you want to use it is not very comfortable. Brannigan provides a simple mechanism to create custom, named validation methods that can be used across schemes as if they were internal methods.

The process is simple: when creating your schemes, give the names of the custom validation methods and their relevant supplement values as with every built-in validation method. For example, suppose we want to create a custom validation method named 'forbid_words', that makes sure a certain text does not contain any words we don't like it to contain. Suppose this will be true for a parameter named 'text'. Then we define 'text' like so:

    text => {
            required => 1,
            forbid_words => ['curse_word', 'bad_word', 'ugly_word'],
    }

As you can see, we have provided the name of our custom method, and the words we want to forbid. Now we need to actually create this forbid_words() method. We do this after we've created our Brannigan object, by using the custom_validation() method, as in this example:

    $b->custom_validation('forbid_words', sub {
            my ($value, @forbidden) = @_;

            foreach (@forbidden) {
                    return 0 if $value =~ m/$_/;
            }

            return 1;
    });

We give the custom_validation() method the name of our new method, and an anonymous subroutine, just like in "local" custom validation methods.

And that's it. Now we can use the forbid_words() validation method across our schemes. If a paremeter failed our custom method, it will be added to the rejects like built-in methods. So, if 'text' failed our new method, our rejects hash-ref will contain:

    text => [ 'forbid_words(curse_word, bad_word, ugly_word)' ]

As an added bonus, you can use this mechanism to override Brannigan's built-in validations. Just give the name of the validation method you wish to override, along with the new code for this method. Brannigan gives precedence to cross-scheme custom validations, so your method will be used instead of the internal one.

NOTES ABOUT PARSE METHODS

As stated earlier, your parse() methods are expected to return a hash-ref of key-value pairs. Brannigan collects all of these key-value pairs and merges them into one big hash-ref (along with all the non-parsed parameters).

Brannigan actually allows you to have your parse() methods be two-leveled. This means that a value in a key-value pair in itself can be a hash-ref or an array-ref. This allows you to use the same key in different places, and Brannigan will automatically aggregate all of these occurrences, just like in the first level. So, for example, suppose your scheme has a regex rule that matches parameters like 'tag_en' and 'tag_he'. Your parse method might return something like { tags => { en => 'an english tag' } } when it matches the 'tag_en' parameter, and something like { tags => { he => 'a hebrew tag' } } when it matches the 'tag_he' parameter. The resulting hash-ref from the process method will thus include { tags => { en => 'an english tag', he => 'a hebrew tag' } }.

Similarly, let's say your scheme has a regex rule that matches parameters like 'url_1', 'url_2', etc. Your parse method might return something like { urls => [$url_1] } for 'url_1' and { urls => [$url_2] } for 'url_2'. The resulting hash-ref in this case will be { urls => [$url_1, $url_2] }.

Take note however that only two-levels are supported, so don't go crazy with this.

SO HOW DO I PROCESS INPUT?

OK, so we have created our scheme(s), we know how schemes look and work, but what now?

Well, that's the easy part. All you need to do is call the process() method on the Brannigan object, passing it the name of the scheme to enforce and a hash-ref of the input parameters/data structure. This method will return a hash-ref back, with all the parameters after parsing. If any validations failed, this hash-ref will have a '_rejects' key, with the rejects hash-ref described earlier. Remember: Brannigan doesn't raise any errors. It's your job to decide what to do, and that's a good thing.

Example schemes, input and output can be seen in Brannigan::Examples.

CONSTRUCTOR

new( \%scheme | @schemes )

Creates a new instance of Brannigan, with the provided scheme(s) (see "HOW SCHEMES LOOK" for more info on schemes).

OBJECT METHODS

add_scheme( \%scheme | @schemes )

Adds one or more schemes to the object. Every scheme hash-ref should have a name key with the name of the scheme. Existing schemes will be overridden. Returns the object itself for chainability.

process( $scheme, \%params )

Receives the name of a scheme and a hash-ref of input parameters (or a data structure), and validates and parses these paremeters according to the scheme (see "HOW SCHEMES LOOK" for detailed information about this process).

Returns a hash-ref of parsed parameters according to the parsing scheme, possibly containing a list of failed validations for each parameter.

Actual processing is done by Brannigan::Tree.

process( \%scheme, \%params )

Same as above, but takes a scheme hash-ref instead of a name hash-ref. That basically gives you a functional interface for Brannigan, so you don't have to go through the regular object oriented interface. The only downsides to this are that you cannot define custom validations using the custom_validation() method (defined below) and that your scheme must be standalone (it cannot inherit from other schemes). Note that when directly passing a scheme, you don't need to give the scheme a name.

custom_validation( $name, $code )

Receives the name of a custom validation method ($name), and a reference to an anonymous subroutine ($code), and creates a new validation method with that name and code, to be used across schemes in the Brannigan object as if they were internal methods. You can even use this to override internal validation methods, just give the name of the method you want to override and the new code.

CAVEATS

Brannigan is still in an early stage. Currently, no checks are made to validate the schemes built, so if you incorrectly define your schemes, Brannigan will not croak and processing will probably fail. Also, there is no support yet for recursive inheritance or any crazy inheritance situation. While deep inheritance is supported, it hasn't been tested extensively. Also bugs are popping up as I go along, so keep in mind that you might encounter bugs (and please report any if that happens).

IDEAS FOR THE FUTURE

The following list of ideas may or may not be implemented in future versions of Brannigan:

SEE ALSO

Brannigan::Validations, Brannigan::Tree, Brannigan::Examples.

AUTHOR

Ido Perlmuter, <ido at ido50 dot net>

BUGS

Please report any bugs or feature requests to bug-brannigan at rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Brannigan. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Brannigan

You can also look for information at:

ACKNOWLEDGEMENTS

Brannigan was inspired by Oogly (Al Newkirk) and the "Ketchup" jQuery validation plugin (http://demos.usejquery.com/ketchup-plugin/).

LICENSE AND COPYRIGHT

Copyright 2017 Ido Perlmuter

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.