Params::Filter

Fast field filtering for parameter construction.

Description

Params::Filter provides fast, lightweight parameter filtering that checks only for the presence or absence of specified fields. It does NOT validate values - no type checking, truthiness testing, or lookups.

This module separates field filtering from value validation:

This approach handles common parameter issues:

When to Use This Module

This module is useful when you have:

When NOT to Use This Module

If you're constructing both the filter rules AND the data structure at the same point in your code, you probably don't need this module. The module's expected use is to apply pre-defined rules to data that may be inconsistent or incomplete for its intended use. If there isn't repetition or an unknown/unreliable data structure, this might be overkill.

This Module Does NOT Do Fancy Stuff

As much as this module attempts to be versatile in usage, there are some VERY HANDY AFFORDANCES IT DOES NOT PROVIDE:

Installation

perl Makefile.PL
make
make test
make install

Usage

Functional Interface

use Params::Filter;    # auto-imports filter() subroutine

# Define filter rules
my @required_fields = qw(name email);
my @accepted_fields = qw(phone city state zip);
my @excluded_fields = qw(ssn password);

# Apply filter to incoming data (from web form, CLI, API, etc.)
my ($filtered_data, $status) = filter(
    $incoming_params,    # Data from external source
    \@required_fields,
    \@accepted_fields,
    \@excluded_fields,
);

if ($filtered_data) {
    # Success - use filtered data
    process_user($filtered_data);
} else {
    # Error - missing required fields
    die "Validation failed: $status";
}

Object-Oriented Interface

use Params::Filter;

my $user_filter = Params::Filter->new_filter({
    required => ['username', 'email'],
    accepted => ['first_name', 'last_name', 'phone', 'bio'],
    excluded => ['password', 'ssn', 'credit_card'],
});

# Apply same filter to multiple incoming datasets
my ($user1, $msg1) = $user_filter->apply($web_form_data);
my ($user2, $msg2) = $user_filter->apply($api_request_data);
my ($user3, $msg3) = $user_filter->apply($db_record_data);

Modifier Methods for Dynamic Configuration

The OO interface provides methods to modify a filter's configuration after creation.

# Start with an empty filter (rejects all by default)
my $filter = Params::Filter->new_filter();

# Configure it in steps as needed
$filter->set_required(['id', 'name']);
# later:
$filter->set_accepted(['email', 'phone'])
$filter->set_excluded(['password']);

# Or use method chaining for one-liner setup
my $filter = Params::Filter->new_filter()
    ->set_required(['user_id'])
    ->accept_all()  # Convenience method for wildcard
    ->set_excluded(['api_key']);

Available Modifier Methods

Important Behavior Notes

Empty Calls Set to Empty Arrays: If no fields are provided to set_required(), set_accepted(), or set_excluded(), the respective list is set to an empty array []:

$filter->set_accepted();  # Sets accepted to `[]`
# Result: Only required fields will be accepted (extras rejected)

Method Chaining: All modifier methods return $self for chaining:

$filter->set_required(['id'])
        ->set_accepted(['name'])
        ->accept_all();  # Overrides set_accepted

Mutability: A filter may call its modifier methods more than once, and the changes take effect immediately.

Meta-Programming Use Cases: These methods enable dynamic configuration for conditional scenarios:

# Environment-based configuration
my $filter = Params::Filter->new_filter();

if ($ENV{MODE} eq 'production') {
    $filter->set_required(['api_key', 'endpoint'])
              ->set_accepted(['timeout', 'retries'])
              ->set_excluded(['debug_info']);
}
else {
    $filter->set_required(['debug_mode'])
              ->accept_all();
}

# Dynamic field lists from config
my $config_fields = load_config('fields.json');
$filter->set_required($config_fields->{required})
          ->set_accepted($config_fields->{accepted})
          ->set_excluded($config_fields->{excluded});

Features

Parameters

filter($args, $required, $accepted, $excluded, $debug)

Returns

In scalar context: hashref with filtered parameters, or undef on failure

In list context: (hashref with filtered parameters, status_message) or (undef, error_message)

Wildcard Feature

The accepted parameter supports a wildcard '*' to accept all fields (except those in excluded).

Wildcard Usage

# Accept all fields
filter($input, [], ['*']);

# Accept all fields except specific exclusions
filter($input, [], ['*'], ['password', 'ssn']);

# Required + all other fields
filter($input, ['id', 'name'], ['*']);

# Wildcard can appear anywhere in accepted list
filter($input, [], ['name', 'email', '*']);  # debugging: add '*' to see everything
filter($input, [], ['*', 'phone', 'address']);

Important Notes

Debugging Pattern

A common debugging pattern is to add '*' to an existing accepted list:

# Normal operation
filter($input, ['id'], ['name', 'email']);

# Debugging - see all inputs
filter($input, ['id'], ['name', 'email', '*']);

Examples

Basic Form Validation

use Params::Filter;

# Define filtering rules (could be from config file)
my @required = qw(name email);
my @accepted = qw(phone city state zip);

# Apply to incoming web form data
my ($user_data, $status) = filter(
    $form_submission,   # Data from web form
    \@required,
    \@accepted,
);

if ($user_data) {
    register_user($user_data);
} else {
    show_error($status);
}

Reusable Filter for Multiple Data Sources

# Create filter once
my $user_filter = Params::Filter->new_filter({
    required => ['username', 'email'],
    accepted => ['full_name', 'phone', 'bio'],
    excluded => ['password', 'ssn', 'credit_card'],
});

# Apply to multiple incoming datasets
my ($user1, $msg1) = $user_filter->apply($web_form_data);
my ($user2, $msg2) = $user_filter->apply($api_request_data);
my ($user3, $msg3) = $user_filter->apply($csv_import_data);

Environment-Specific Filtering

my $filter = Params::Filter->new_filter();

if ($ENV{APP_MODE} eq 'production') {
    # Strict: only specific fields allowed
    $filter->set_required(['api_key'])
          ->set_accepted(['timeout', 'retries'])
          ->set_excluded(['debug_info', 'verbose']);
} else {
    # Development: allow everything
    $filter->set_required(['debug_mode'])
          ->accept_all();
}

my ($config, $msg) = $filter->apply($incoming_config);

Security Filtering

# Remove sensitive fields from user input
my ($safe_data, $msg) = filter(
    $user_input,
    ['username', 'email'],           # required
    ['full_name', 'phone', 'bio'],    # accepted
    ['password', 'ssn', 'api_key'],   # excluded
);

# Result contains only safe fields
# password, ssn, api_key are removed even if provided

Dynamic Configuration from File

# Load filter rules from config file
my $config = decode_json(`cat filters.json`);

my $filter = Params::Filter->new_filter()
    ->set_required($config->{user_create}{required})
    ->set_accepted($config->{user_create}{accepted})
    ->set_excluded($config->{user_create}{excluded});

# Apply to incoming data
my ($filtered, $msg) = $filter->apply($api_data);

Data Segregation for Multiple Subsystems

A common pattern is splitting incoming data into subsets for different handlers or storage locations. Each filter extracts only the fields needed for its specific purpose, implementing security through compartmentalization.

# Three different forms collect overlapping data:

# Main subscription form collects: 
#  name, email, zip, 
#  user_id, password, credit_card_number, subscription_term

# Subscriber profile form collects: 
#  name, email, address, city, state, zip, 
#  user_id, password, credit_card_number, 
#  phone, occupation, position, education 
#  alt_card_number, billing_address, billing_zip

# Promo subscription form collects: 
#  name, email, zip, subscription_term, 
#  user_id, password, credit_card_number, promo_code

my $data = $webform->input(); # From any of the above

# Personal data filter - general user info (no sensitive data)
my $person_filter = Params::Filter->new_filter({
    required => ['name', 'user_id', 'email'],
    accepted => ['address', 'city', 'state', 'zip', 'phone', 
                 'occupation', 'position', 'education'],
    excluded => ['password', 'credit_card_number'],
});

# Business data filter - subscription and billing info
my $biz_filter = Params::Filter->new_filter({
    required => ['user_id', 'subscription_term', 'credit_card_number', 'zip'],
    accepted => ['alt_card_number', 'billing_address', 'billing_zip', 'promo_code'],
    excluded => ['password'],
});

# Authentication data filter - only credentials
my $auth_filter = Params::Filter->new_filter({
    required => ['user_id', 'password'],
    accepted => [],
    excluded => [],
});

# Apply all filters to the same web form submission
my ($person_data, $pmsg) = $person_filter->apply($data);
my ($biz_data,    $bmsg) = $biz_filter->apply($data);
my ($auth_data,   $amsg) = $auth_filter->apply($data);

unless ($person_data && $biz_data && $auth_data) {
    return "Unable to add user: " .
        join ' ' => grep { $_ ne 'Admitted' } ($pmsg, $bmsg, $amsg);
}

# Collect any debug warnings from successful filters 
# if the filter's `debug` parameter is 'true' (1)
my @warnings = grep { $_ ne 'Admitted' } ($pmsg, $bmsg, $amsg);
warn "Params filter debug warnings:\n" . join("\n", @warnings) . "\n"
    if @warnings;

# Route each subset to appropriate handler
$self->add_user($person_data);           # User profile
$self->set_subscription($biz_data);       # Billing system
$self->set_password($auth_data);          # Auth system

NOTE: The original $data is not modified by any filter. Each call to apply() creates its own internal copy, so the same data can be safely processed by multiple filters.

More Examples

See the examples/ directory for complete working scripts:

Author

Bruce Van Allen bva@cruzio.com

License

perl_5

Copyright (C) 2026, Bruce Van Allen