NAME

Brannigan - Flexible library for validating and processing input.

SYNOPSIS

use Brannigan;

my %schema1 = ( params => ... );
my %schema2 = ( params => ... );
my %schema3 = ( params => ... );

# use the OO interface
my $b = Brannigan->new();
$b->register_schema('schema1', \%schema1);
$b->register_schema('schema2', \%schema2);
$b->register_schema('schema3', \%schema3);

my $rejects = $b->process('schema1', \%params);
if ($rejects) {
    die $rejects;
}

# %params is valid and ready for use.

# Or use the functional interface
my $rejects = Brannigan::process(\%schema1, \%params);
if ($rejects) {
    die $rejects;
}

For a more comprehensive example, see "MANUAL".

DESCRIPTION

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

  • Simple User Input

    Brannigan can validate and process simple, "flat" user input, possibly coming from web forms.

  • Complex Data Structures

    Brannigan can validate and process complex data structures, possibly deserialized from JSON or XML data sent to web services and APIs.

Brannigan's approach to data validation is as follows: define a schema of parameters and their validation rules, and let the module automatically examine input parameters against this structure. Brannigan provides you with common validators that are used everywhere, and also allows you to create custom validators easily. This structure also defines how, if at all, the input should be processed. 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 schema. Schemas can extend other schemas, allowing you to be much more flexible in certain situations. Imagine you have a blogging application. A base schema might define all validations and processing needed in order to create a new blog post from user input. When editing a post, however, some parameters that were required when creating the post might not be required now, and maybe new parameters are introduced. Inheritance helps you avoid repeating yourself.

MANUAL

Let's look at a complete usage example. Do not be alarmed by the size of these schemas, as they showcases almost all features of Brannigan.

package MyApp;

use strict;
use warnings;
use Brannigan;

# Create a new Brannigan object
my $b = Brannigan->new({ handle_unknown => "ignore" });

# Create a custom 'forbid_words' validator that can be used in any schema.
$b->register_validator('forbid_words', sub {
    my $value = shift;

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

    return 1;
});

# Create a schema for validating input to a create_post function
$b->register_schema('create_post', {
    params => {
        subject => {
            required => 1,
            length_between => [3, 40],
        },
        text => {
            required => 1,
            min_length => 10,
            validate => sub {
                my $value = shift;
                return defined $value && $value =~ m/^lorem ipsum/ ? 1 : 0;
            }
        },
        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],
            postprocess => sub {
                my $val = shift;

                return $val == 1 ? 'reviews' :
                       $val == 2 ? 'receips' : 'general';
            },
        },
        id => {
            required => 1,
            exact_length => 10,
            value_between => [1000000000, 2000000000],
        },
        array_of_ints => {
            array => 1,
            min_length => 3,
            values => {
                integer => 1,
            },
            preprocess => sub {
                # Sometimes you'll find that input that is supposed to be
                # an array is received as a single non-array item, most
                # often because deserializers do not know the item should
                # be in an array. This is common in XML inputs. A
                # preprocess function can be used to fix that.
                my $val = shift;
                return [$val]
                    if defined $val && ref $val ne 'ARRAY';
                return $val;
            }
        },
        hash_of_langs => {
            hash => 1,
            keys => {
                en => {
                    required => 1,
                },
            },
        },
    },
});

# Create a schema for validating input to an edit_post function. The schema
# inherits the create_post schema with one small change.
$b->register_schema('edit_post', {
    inherits_from => 'create_post',
    params => {
        subject => {
            required => 0, # subject is no longer required
        }
    }
});

# Now use Brannigan to validate input in your application:
sub create_post {
    my ($self, $params) = @_;

    # Process and validate the parameters with the 'post' schema
    my $rejects = $b->process('create_post', $params);

    if ($rejects) {
        # Turn validation errors into a structure that fits your application
        die list_errors($rejects);
    }

    # Validation and processing suceeded, save the parameters to a database
    $self->_save_post_to_db($params);
}

sub edit_post {
    my ($self, $id, $params) = @_;

    # Process and validate the parameters with the 'edit_post' schema
    my $rejects = $b->process('edit_post', $params);

    if ($rejects) {
        # Turn validation errors into a structure that fits your application
        die list_errors($rejects);
    }

    # Validation and processing succeeded, update the post in the database
    $self->_update_post_in_db($params);
}

HOW BRANNIGAN WORKS

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

1. SCHEMA PREPARATION

Brannigan receives the name of a validation schema, and a hash reference of input parameters. Brannigan then loads the schema and prepares it (merging it with inherited schemas, if any) for later processing. Finalized schemas are cached for improved performance.

2. DATA PREPROCESSING

Brannigan invokes all preprocess functions defined in the schema on the input data, if there are any. These functions are allowed to modify the input.

Configured default values will also be provided to their respective parameters in this stage as well, if those parameters are not provided in the input.

3. DATA VALIDATION

Brannigan invokes all validation methods defined in the schema on the input data, and generates a hash reference of rejected parameters, if there were any. For every parameter in this hash-ref, an array-ref of failed validations is created.

If one or more parameters failed validation, the next step (data postprocessing) will be skipped.

4. DATA POSTPROCESSING

If the previous stage (validation) did not fail, Brannigan will call every postprocess function defined in the schema. There are two types of postprocess functions:

  • parameter-specific

    These are defined on specific parameters. They get the parameter's value and should return a new value for the parameter (possibly the same one, but they must return a value).

  • global

    The schema may also have one global postprocess function. This function gets the entire parameter hash-ref as input. It is free to modify the hash-ref as it sees fit. The function should not return any value.

5. FINAL RESULT

If all input parameters passed validation, an undefined value is returned to the caller. Otherwise, a hash-reference of rejects is returned. This is a flattened structure where keys are "fully qualified" parameter names (meaning dot notation is used for nested parameters), and values are hash-references containing the validators for which the parameter had failed. For example, let's look at the following rejects hash-ref:

{
    'subject'      => { required => 1 },
    'text'         => { max_length => 500 },
    'pictures.2'   => { matches => qr!^http://! },
    'phone.mobile' => { required => 1 }
}

This hash-ref tells us:

1. The "subject" parameter is required but was not provided.
2. The "text" parameter was provided, but is longer than the maximum of 500 characters.
3. The third value of the "pictures" array does not start with "http://".
4. The "mobile" key of the "phone" hash parameter was not provided.

HOW SCHEMAS LOOK

The validation/processing schema defines the structure of the data you're expecting to receive, along with information about the way it should be validated and processed. Schemas are created by passing them to the Brannigan constructor. You can pass as many schemas as you like, and these schemas can inherit from other schemas.

A schema is a hash-ref that contains the following keys:

  • inherits_from

    Either a scalar naming a different schema or an array-ref of schema names. The new schema will inherit all the properties of the schema(s) defined by this key. If an array-ref is provided, the schema will inherit their properties in the order they are defined. See the "CAVEATS" section for some "heads-up" about inheritance.

  • params

    Defines the expected input. This key takes a hash-ref whose keys are the names of input parameters as they are expected to be received. The values are also hash references which define the necessary validation functions to assert for the parameters, and other optional settings such as default values, post- and pre- processing functions, and custom validation functions.

    For example, if a certain parameter, let's say 'subject', must be between 3 to 10 characters long, then your schema will contain:

    subject => { length_between => [3, 10] }

    If a "subject" parameter sent to your application fails the "length_between" validator, then the rejects hash-ref described earlier will have the exact same key-value pair as above:

    subject => { length_between => [3, 10] }

    The following extra keys can also be used in a parameter's configuration:

    validate: Used to create a custom validation function for the parameter. Accepts a subroutine reference. The subroutine accepts the value from the input as its only parameter, and returns a boolean value indicating whether the value passed the validation or not.

    For example, this custom validation function requires that the 'subject' input parameter will always begin with the string "lorem ipsum":

    subject => {
        length_between => [3, 10],
        validate => sub {
            my $value = shift;
            return $value =~ m/^lorem ipsum/ ? 1 : 0;
        }
    }

    If a parameter fails a custom validation function, 'validate' will be added to the failed validations hash-ref of the parameter in the rejects hash-ref:

    subject => {
        length_between => [3, 10],
        validate => 1
    }

    default: Used to set a default value for parameters that are not required and are not provided in the input hash-ref. Accepts a scalar value or a subroutine reference. In the latter case, the subroutine will be called with no parameters, and it should return the generated default value.

    subject => {
        length_between => [3, 10],
        default => 'lorem ipsum'
    }
    
    # Or...
    
    subject => {
        length_between => [3, 10],
        default => sub { UUID->new->hex }
    }

    Note that default values are given to missing parameters before the validation stage, meaning they must conform with the parameters' validators.

    preprocess: Used to process parameter values before validation functions are called. This can be useful to trim leading or trailing whitespace from string values, or turning scalars into arrays (a common task for XML inputs where the deserializer cannot tell whether an item actually belongs in an array or not). Accepts a subroutine reference with the parameter's value from the input. The function must return the new value for the parameter, even if it had decided not to do any actual changes.

    postprocess: Similar to preprocess, but happens after validation functions had been called.

    subject => {
        required => 1,
        length_between => [3, 10],
        preprocess => sub {
            # Trim whitespace before validating
            my $value = shift;
            $value =~ s/^\s\*//;
            $value =~ s/\s\*$//;
            return $value;
        }
        validate => sub {
            # Ensure value does not start with "lorem ipsum"
            my $value = shift;
            return $value =~ m/^lorem ipsum/ ? 0 : 1;
        },
        postprocess => sub {
            # Lowercase the value
            my $value = shift;
            return lc $value;
        }
    }
  • postprocess

    Global postprocessing function. If provided, it will be called after all preprocessing, input validation, and parameter-specific postprocessing had completed. As opposed to parameter-specific postprocess functions, this one receives the complete parameter hash-ref as its only input. It is not expected to return any values. It may modify the parameter hash-ref as it sees fit.

BUILT-IN VALIDATORS

{ required => $boolean }

If $boolean has a true value, this method will check that a required parameter was indeed provided; otherwise (i.e. if $boolean is not true) this method will simply return a true value to indicate success.

You should note that if a parameter is required, and a non-true value is received (i.e. 0 or the empty string ""), this method considers the requirement as fulfilled (i.e. it will return true). If you need to make sure your parameters receive true values, take a look at the is_true() validation method.

Please note that if a parameter is not required and indeed isn't provided with the input parameters, any other validation methods defined on the parameter will not be checked.

{ is_true => $boolean }

If $boolean has a true value, this method will check that $value has a true value (so, $value cannot be 0 or the empty string); otherwise (i.e. if $boolean has a false value), this method does nothing and simply returns true.

{ length_between => [ $min_length, $max_length ] }

Makes sure the value's length (stringwise) is inside the range of $min_length-$max_length, or, if the value is an array reference, makes sure it has between $min_length and $max_length items.

{ min_length => $min_length }

Makes sure the value's length (stringwise) is at least $min_length, or, if the value is an array reference, makes sure it has at least $min_length items.

{ max_length => $max_length }

Makes sure the value's length (stringwise) is no more than $max_length, or, if the value is an array reference, makes sure it has no more than $max_length items.

{ exact_length => $length }

Makes sure the value's length (stringwise) is exactly $length, or, if the value is an array reference, makes sure it has exactly $exact_length items.

{ integer => $boolean }

If boolean is true, makes sure the value is an integer.

{ function => $boolean }

If boolean is true, makes sure the value is a function (subroutine reference).

{ value_between => [ $min_value, $max_value ] }

Makes sure the value is between $min_value and $max_value.

{ min_value => $min_value }

Makes sure the value is at least $min_value.

{ max_value => $max_value }

Makes sure the value is no more than $max_value.

{ array => $boolean }

If $boolean is true, makes sure the value is actually an array reference.

{ hash => $boolean }

If $boolean is true, makes sure the value is actually a hash reference.

{ one_of => \@values }

Makes sure a parameter's value is one of the provided acceptable values.

{ matches => $regex }

Returns true if $value matches the regular express (qr//) provided. Will return false if $regex is not a regular expression.

{ min_alpha => $integer }

Returns a true value if $value is a string that has at least $integer alphabetic (A-Z and a-z) characters.

{ max_alpha => $integer }

Returns a true value if $value is a string that has at most $integer alphabetic (A-Z and a-z) characters.

{ min_digits => $integer }

Returns a true value if $value is a string that has at least $integer digits (0-9).

{ max_digits => $integer }

Returns a true value if $value is a string that has at most $integer digits (0-9).

{ min_signs => $integer }

Returns a true value if $value has at least $integer special or sign characters (e.g. %^&!@#, or basically anything that isn't A-Za-z0-9).

{ max_signs => $integer }

Returns a true value if $value has at most $integer special or sign characters (e.g. %^&!@#, or basically anything that isn't A-Za-z0-9).

{ max_consec => $integer }

Returns a true value if $value does not have a sequence of consecutive characters longer than $integer. Consequtive characters are either alphabetic (e.g. abcd) or numeric (e.g. 1234).

{ max_reps => $integer }

Returns a true value if $value does not contain a sequence of a repeated character longer than $integer. So, for example, if $integer is 3, then "aaa901" will return true (even though there's a repetition of the 'a' character it is not longer than three), while "9bbbb01" will return false.

ADVANCED FEATURES AND TIPS

COMPLEX DATA STRUCTURES

Brannigan can validate and process hash references of arbitrary complexity. Input parameters may also be hash or array references.

For arrays, the parameter needs to be marked with array => 1. The validations and processing for the array's values are then provided as a hash reference named values. For example:

pictures => {
    array => 1,
    length_between => [1, 5],
    values => {
        min_length => 3,
        validate => sub {
            my $value = shift;
            return $value =~ m!^http://! ? 1 : 0;
        }
    }
}

In this example, "pictures" is an array parameter. When provided, the array must contain between 1 and 5 items. Every item in the array must be a string of 3 characters or more, and must begin with the prefix "http://".

For hashes, the parameter needs to be marked with hash => 1. The validations and processing for the hash's attributes are then provided as a hash reference named keys. For example:

name => {
    hash => 1,
    keys => {
        first_name => {
            length_between => [3, 10],
        },
        last_name => {
            required => 1,
            min_length => 3
        }
    }
}

In this example, "name" is a hash paremeter. When provided, it must contain an attribute called "first_name", which is an optional string between 3 or 10 characters long, and "last_name", which is a required string at least 3 characters longs.

Array and hash parameters can also accept default values:

complex_param => {
    hash => 1,
    keys => {
        ...
    },
    default => { key1 => 'def1', key2 => 'def2' }
}

Hash and arrays can fail validation in two ways: they can fail as a unit (for example, schemas can enforce that an array will have between 2 and 5 items), and specific items within them can fail (for example, schemas can enforce that items in an array will be integers lower than 100).

An array that failed as a unit will appear in the rejects hash-ref with its own name. A specific array item or hash key that failed validation will appear with dot notation:

'name.first_name' => { length_between => [3, 10] },
'name.last_name' => { required => 1 },
'pictures' => { exact_length => 3 },
'numbers.1' => { max_value => 10 },

In this example, specific keys failed in the "name" hash parameter. The "pictures" array parameter failed as a unit (it should have exactly 3 items). The second item in the "numbers" array parameter failed the "max_value" validator too.

Brannigan's data structure support is infinitely recursive:

pictures => {
    array => 1,
    values => {
        hash => 1,
        keys => {
            filename => {
                min_length => 5,
            },
            source => {
                hash => 1,
                keys => {
                    website => {
                        validate => sub { ... },
                    },
                    license => {
                        one_of => [qw/GPL FDL CC/],
                    },
                },
            },
        },
    },
}

CROSS-SCHEMA CUSTOM VALIDATION METHODS

Ad-hoc validate functions are nice, but when you want to use the same custom validation function in multiple places inside your schema (or in multiple schemas), this can become unwieldy.

Brannigan provides a simple mechanism to create custom, named validation functions that can be used across schemas as if they were internal methods.

This example creates a validation function called "forbid_words", which fails string parameters that contain certain words:

my $b = Brannigan->new();

$b->register_validator('forbid_words', sub {
    my ($value, @forbidden) = @_;
    foreach (@forbidden) {
        return 0 if $value =~ m/$_/;
    }
    return 1;
});

$b->register_schema('user_input', {
    params => {
        text => {
            required => 1,
            forbid_words => ['curse_word', 'bad_word', 'ugly_word'],
        }
    }
});

Note how the custom validation function accepts the value provided in the input, and whatever was provided to 'forbid_words' in the configuration of the specific parameter. In this case, the parameter called "text" forbids the words "curse_word", "bad_word" and "ugly_word".

If a parameter fails a named custom validation function, it will be added to the rejects hash-ref like any other built-in validation function:

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.

Note that you do not have to register a named validator before you register a schema that uses it. You can register the schema first.

REPEATING RULES FOR MULTIPLE PARAMETERS

In previous versions, Brannigan allowed providing rules to multiple parameters via regular expressions. This feature has been removed in version 2.0. Instead, users can take advantage of the fact that schemas are simply Perl structures and reuse rules via variables:

my $date = { required => 1, matches => qr/^\d{4}-\d{2}-\d{2}$/ };

my $schema = {
    name => 'person',
    params => {
        birth_date => $date,
        death_date => $date
    }
};

CONSTRUCTOR

new( [ %options ] )

Creates a new instance of Brannigan. Schemas must be registered separately using the register_schema method.

Options:

  • handle_unknown

    What to do with input parameters that are not defined in the processing schema. Values: 'ignore' (default, keep unknown parameters as they are), 'remove' (delete unknown parameters from the input), 'reject' (add to rejects and fail the processing).

OBJECT METHODS

register_schema( $name, \%schema )

Registers a validation schema with the given name. If a schema with the same name already exists, it will be overridden. The schema hash-ref should not contain a name key as it's provided separately. Returns the Brannigan object itself for chain-ability.

register_validator( $name, $code )

Registers a new named validator function. $code is a reference to a subroutine that receives a value as a parameter and returns a boolean value indicating whether the value is valid or not. The method can be used to override built-in validation functions.

handle_unknown( [$value] )

Gets or sets the behavior for handling unknown input parameters. Accepted values: 'ignore', 'remove', 'reject'.

process( $schema, \%params )

Receives the name of a schema and a hash reference of input parameters. Performs pre-processing, validation and post-processing as described in the manual.

Any processing that modifies the input is performed in-place.

Returns an undefined value if there were no rejects. Returns a hash reference of rejects if there were any.

FUNCTIONAL INTERFACE

process( \%schema, \%params )

Accepts a schema hash-ref and an input hash-ref, and performs pre-processing, validation and post-processing. If no parameters failed validation, an undefined value is returned. Otherwise a hash reference of rejects is returned.

Note that this interface does not allow for custom validation functions and schema inheritance. You are not required to give the schema a name when using this interface.

my $rejects = Brannigan::process( $schema, $params );

UPGRADING FROM 1.x TO 2.0

Version 2.0 of Brannigan includes significant breaking changes. This guide will help you upgrade your existing code.

BREAKING CHANGES

Constructor and Schema Registration

Old (1.x):

my $b = Brannigan->new(
    { name => 'schema1', params => { ... } },
    { name => 'schema2', params => { ... } }
);

New (2.0):

my $b = Brannigan->new();  # No schemas in constructor
$b->register_schema('schema_name', { params => { ... } });
$b->register_schema('another_schema', { params => { ... } });

Method Names

Several methods have been renamed for clarity:

  • add_scheme() => register_schema()

  • custom_validation() => register_validator()

Return Value Changes

The process() method now returns different values:

Old (1.x): Always returned a hash-ref with processed parameters and optional _rejects key.

New (2.0): Returns undef on success, hash-ref of rejects on failure. Processing happens in-place on the input hash-ref.

# Old style
my $result = $b->process('schema', \%params);
if ($result->{_rejects}) {
    # Handle errors
}

# New style
my $rejects = $b->process('schema', \%params);
if ($rejects) {
    # Handle $rejects hash-ref directly
}
# %params is modified in-place with processed values

Error Structure Changes

The structure of validation errors has changed significantly:

Old (1.x):

{
    _rejects => {
        parameter => ['required(1)', 'min_length(5)'],
        nested    => { path => { param => ['max_value(100)'] } }
    }
}

New (2.0):

{
    'parameter'         => { required => 1, min_length => 5 },
    'nested.path.param' => { max_value => 100 }
}

Key changes:

  • Error paths are flattened using dot notation

  • Validator names and arguments are returned as key-value pairs

  • No more _rejects wrapper

  • Unknown parameters are reported with { unknown => 1 }

Processing Function Changes

  • parse functions → postprocess functions

  • Default values are now calculated before validation (they can fail validation)

  • postprocess functions must return a replacement value, not a hash-ref

Old (1.x):

parse => sub {
    my $value = shift;
    return { parameter_name => process($value) };
}

New (2.0):

postprocess => sub {
    my $value = shift;
    return process($value);  # Return the processed value directly
}

NEW FEATURES

Preprocessing

You can now preprocess input before validation:

params => {
    username => {
        preprocess => sub { lc },  # Lowercase parameter value
        required => 1,
        min_length => 3
    }
}

Global Postprocessing

Add a global postprocess function to your schema:

{
    params => { ... },
    postprocess => sub {
        my $params = shift;
        $params->{computed_field} = calculate($params);
        # Modify $params in-place, no return value needed
    }
}

Unknown Parameter Handling

Control how unknown parameters are handled:

my $b = Brannigan->new();
$b->handle_unknown('ignore');  # Default: preserve unknown params
$b->handle_unknown('remove');  # Remove unknown params
$b->handle_unknown('reject');  # Fail validation on unknown params

This works at all nesting levels (top-level, nested hashes, array items).

Enhanced Default Values

Default values now work in nested structures:

params => {
    users => {
        array => 1,
        values => {
            hash => 1,
            keys => {
                name => { required => 1 },
                role => { default => 'user' }, # Applied to each array item
                active => { default => 1 }
            }
        }
    }
}

Improved Schema Inheritance

Schema inheritance now works recursively and merges parameter definitions:

$b->register_schema('base', {
    params => {
        name => { required => 1, max_length => 50 }
    }
});

$b->register_schema('extended', {
    inherits => 'base',
    params => {
        name => { min_length => 2 },  # Merges with base constraints
        email => { required => 1 }    # Additional parameter
    }
});

REMOVED FEATURES

The following features have been removed:

  • Parameter groups (use global postprocess instead)

  • Regular expression parameter definitions

  • Scope-local "_all" validators

  • max_dict validator

  • forbidden validator (use handle_unknown => 'reject' instead)

  • ignore_missing schema option (use handle_unknown instead)

AUTHOR

Ido Perlmuter, <ido at ido50 dot net>

BUGS

Please report any bugs or feature requests to https://github.com/ido50/Brannigan.

SUPPORT

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

perldoc Brannigan

ACKNOWLEDGEMENTS

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

LICENSE AND COPYRIGHT

Copyright 2025 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.

1 POD Error

The following errors were encountered while parsing the POD:

Around line 1669:

Non-ASCII character seen before =encoding in '→'. Assuming UTF-8