NAME

PAGI::StructuredParameters - Whitelist and structure incoming request parameters

SYNOPSIS

use PAGI::StructuredParameters;

my $params = PAGI::StructuredParameters->new(
    src      => 'body',
    src_data => {
        username     => 'jnap',
        'name.first' => 'John',
        'name.last'  => 'Napiorkowski',
        'email[0]'   => 'jnap@example.com',
    },
);

my $clean = $params->permitted(
    'username',
    name => ['first', 'last'],
    +{ email => [] },
);

# $clean = {
#   username => 'jnap',
#   name     => { first => 'John', last => 'Napiorkowski' },
#   email    => ['jnap@example.com'],
# }

DESCRIPTION

A no-dependency port of the core of Catalyst::Utils::StructuredParameters, decoupled from Catalyst. Its job is to whitelist and structure incoming parameters before they reach a model: a prior layer to validation, not validation itself. Validation of values is out of scope (use a downstream validator such as Valiant).

Form and query parameters arrive as a flat hash whose keys encode nesting (name.first, email[0], children[0].age). The rule grammar reconstructs the named, present keys into the described nested shape and drops everything else. Already-nested body data (such as decoded JSON) is simply whitelisted.

CONSTRUCTOR

new

my $params = PAGI::StructuredParameters->new(
    src                 => 'body',     # 'body' | 'query' | 'data'
    src_data            => \%params,   # the source hash to filter
    namespace           => \@fields,   # optional starting namespace
    flatten_array_value => $bool,      # default: 1 for body/query, 0 for data
    max_array_depth     => 1000,       # cap on array reconstruction
    context             => $obj,       # optional; passed to required's callback
);

src_data is the materialized source hash. For request-bound, asynchronous sources (a POST body), use the adapters in PAGI::StructuredParameters::Request instead.

REQUEST ADAPTERS

These class methods bind the engine to a live PAGI::Request, returning a PAGI::StructuredParameters::Request whose permitted/required are asynchronous (the request body is read with await).

from_body

my $params = PAGI::StructuredParameters->from_body($req, $context);

Source is the request's form/body parameters (array values flattened by default).

from_query

my $params = PAGI::StructuredParameters->from_query($req, $context);

Source is the URI query string (array values flattened by default).

from_data

my $params = PAGI::StructuredParameters->from_data($req, $context);

Source is the decoded request body data such as JSON (arrays kept as-is).

The optional $context is passed as the first argument to required's on-missing callback.

RULE SPECIFICATIONS

'username', 'password' — scalar keys.
name => ['first', 'last'] — a nested hash, reconstructed from name.first / name.last.
+{ email => [] } — an array, reconstructed from email[0], email[1], ...
+{ children => [['name','age']] } — an array of hashes, reconstructed from children[0].name, children[1].age, ...

A leading arrayref is treated as a starting namespace affix, e.g. $params->permitted(['person'], 'name', 'age').

METHODS

namespace

$params->namespace(['person']);

Scope all subsequent rules under the given key path. Returns the object for chaining.

flatten_array_value

$params->flatten_array_value(0);

Toggle array-value flattening ("pick last"), a common workaround for HTML form controls that submit repeated values. On by default for body/query, off for data. Returns the object for chaining.

max_array_depth

$params->max_array_depth(50);

Cap the number of items reconstructed for any single array, guarding against hostile input. Throws PAGI::StructuredParameters::Exception::InvalidArrayLength when exceeded. Returns the object for chaining.

permitted

my $clean = $params->permitted(@rules);

Lenient whitelist. Returns a clean hashref containing only the permitted, present keys, reconstructed into the shape the rules describe. Absent keys are simply omitted.

required

my $clean = $params->required(@rules, $on_missing);

Strict whitelist. The trailing argument must be a coderef (otherwise a loud required needs an on-missing callback error is raised at the call site).

On success, returns the clean hashref, exactly as "permitted" would. If any required key is absent, $on_missing is invoked as $on_missing->($context, \@missing) — where $context is the object's context attribute (in PAGI::Nano this is the request context $c) and \@missing is an arrayref of every missing dotted key path — and its return value is thrown. PAGI::Nano's dispatch catches that thrown, respond-able value and sends it, making the failure response explicit and chosen at the call site.

Passing the context as an argument (rather than closing over it) lets the callback be a shared, named sub reused across routes.

SEE ALSO

PAGI::StructuredParameters::Request, PAGI::Nano, Catalyst::TraitFor::Request::StructuredParameters.

AUTHOR

John Napiorkowski <jjnapiork@cpan.org>

COPYRIGHT & LICENSE

Copyright 2026, John Napiorkowski.

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.