File Coverage

File:blib/lib/Params/Get.pm
Coverage:100.0%

linestmtbrancondsubtimecode
1package Params::Get;
2
3# Normalises the many calling conventions Perl callers use when passing
4# arguments -- positional scalar, named pairs, hashref, arrayref -- into a
5# single hashref so the receiving sub need not care which style was used.
6
7
11
11
11
739937
8
131
use strict;
8
11
11
11
13
10
192
use warnings;
9
11
11
11
1958
66469
21
use autodie qw(:all);
10
11
11
11
11
81243
31
35
use parent 'Exporter';
12
13
11
11
11
412
7
59
use Carp ();
14
11
11
11
16
5
71
use Scalar::Util ();
15
16
11
11
11
2235
18376
1166
use Readonly;
17
18our @EXPORT_OK = qw(get_params);
19
20 - 28
=head1 NAME

Params::Get - Normalise subroutine arguments regardless of calling convention

=head1 VERSION

Version 0.16

=cut
29
30our $VERSION = '0.16';
31
32# Reference-type sentinels.  Collected here so a typo is a compile-time
33# error via Readonly and grep/ack finds every usage in one search.
34Readonly::Scalar my $T_HASH   => 'HASH';
35Readonly::Scalar my $T_ARRAY  => 'ARRAY';
36Readonly::Scalar my $T_SCALAR => 'SCALAR';
37Readonly::Scalar my $T_CODE   => 'CODE';
38Readonly::Scalar my $T_REF    => 'REF';
39
40 - 202
=head1 DESCRIPTION

C<Params::Get> exports a single function, C<get_params>, which accepts a
caller's argument list (or a reference to it) in any of the common Perl
calling conventions and returns a unified hash-ref.  Library authors can
write one normalisation call at the top of every public method rather than
hand-rolling the same conditional chains in each one.

When combined with L<Params::Validate::Strict> and L<Return::Set> you can
formally specify and enforce the input and output contracts of every method.

=head1 SYNOPSIS

    use Params::Get qw(get_params);
    use Params::Validate::Strict;

    sub where_am_i {
        my $params = Params::Validate::Strict::validate_strict({
            args   => get_params(undef, \@_),
            schema => {
                latitude  => { type => 'number', min => -90,  max =>  90 },
                longitude => { type => 'number', min => -180, max => 180 },
            },
        });
        printf "You are at %s, %s\n",
            $params->{latitude}, $params->{longitude};
    }

    where_am_i(latitude => 0.3, longitude => 124);
    where_am_i({ latitude => 3.14, longitude => -155 });

=head1 METHODS

=head2 get_params

Parse the argument list passed to a subroutine and return a unified hash-ref
regardless of the calling convention used.  Supported conventions:

=over 4

=item * Single hash-ref: C<foo({ a =E<gt> 1 })>

=item * Named key/value pairs: C<foo(a =E<gt> 1, b =E<gt> 2)>

=item * Single scalar with a default key: C<foo('US')> given C<get_params('country', @_)>

=item * Array-ref shorthand: C<foo(\@_)> inside the callee

=item * Mandatory positional argument plus an options hash-ref:
C<Obj-E<gt>new($val, { opt =E<gt> 1 })>

=item * Scalar-ref: C<foo(\'text')> -- dereferenced automatically

=item * Blessed object or CODE ref: mapped under C<$default>

=item * Array-ref of positional key names as C<$default>:
C<get_params([qw(name age)], @_)>

=back

=head3 ARGUMENTS

=over 4

=item C<$default> (scalar string, arrayref of strings, or C<undef>)

Controls how a single non-hash argument is interpreted:

=over 8

=item * B<string> -- used as the key name when a lone scalar, ref, or object
is received.

=item * B<arrayref of strings> -- positional key names; the I<n>th argument
is mapped to the I<n>th name.  Extra arguments are silently discarded;
missing arguments produce C<undef> values.

=item * B<undef> -- no default key; the caller must pass named pairs or a
single hash-ref.  An empty argument list returns C<undef>.

=back

=item C<@args>

The caller's argument list, passed either as a flat list (C<@_>) or as a
reference to the array (C<\@_>).  Both forms are accepted transparently.

=back

=head3 RETURNS

A hash-ref on success, or C<undef> when C<$default> is C<undef> and no
arguments are provided.

=head3 SIDE EFFECTS

Croaks from the caller's frame on programming errors (wrong calling
convention, non-ARRAY ref passed as C<$default>).  Confesses with a full
stack trace when C<$default> is defined but zero arguments are received,
because that almost always indicates a programming error.

=head3 API SPECIFICATION

=head4 Input

        {
                default => {
                        type => [ 'string', 'stringref' ],
                        optional => 1,
                        position => 0,
                }, args => {
                        type => [ 'array', 'arrayref' ],
                        optional => 1,
                        position => 1,
                }
        }

=head4 output

    {
        type => 'hashref',
        optional => 1,
    }

=head3 MESSAGES

    Message                                             Meaning                                Resolution
    --------------------------------------------------  -------------------------------------  ----------------------------------------------
    ::get_params: $default must be a scalar or          A non-ARRAY ref was passed as          Pass a plain string, arrayref of strings,
      arrayref                                          $default                               or undef
    Usage: Pkg->method($key => $val)  [stack trace]    $default is defined but no args given  Ensure the caller always passes a value
    Usage: Pkg->method()                               Odd-length or unrecognisable arg list  Correct the calling convention in the caller

=head3 PSEUDOCODE

    1.  Fast-path: if the sole argument is a plain HASH ref, return it
        immediately (fires before $default is inspected -- see LIMITATIONS).

    2.  Shift $default.  Validate: must be undef, a plain scalar, or an
        ARRAY ref.  Any other ref type croaks immediately.

    3.  If $default is an ARRAY ref, map remaining @_ positionally to those
        key names and return.  A single plain HASH ref is still passed
        through unchanged.

    4.  Detect the \@_ calling convention: if exactly one ARRAY ref argument
        remains, check the two-element (key => scalar-val) shorthand and
        return immediately when it matches.  Otherwise unwrap and use the
        array contents as the effective @args.

    5.  Dispatch on argument count:
        0 -- confess (with stack trace) if $default is defined;
             return undef otherwise.
        1 -- if $default is defined, wrap the single arg under $default
             (scalar, arrayref, scalarref->deref, coderef, blessed object).
             Without $default: unwrap REF-of-REF, pass HASH ref through,
             return empty ARRAY ref as-is.  Anything else: croak.
        2 with HASH ref as arg[1]
          -- Mandatory-positional + options-hashref pattern.
        even N -- treat as flat key/value pairs.
        odd N  -- croak.

=cut
203
204sub get_params
205{
206        # Fast path: sole argument is already a plain hashref.  Returning it
207        # directly avoids the overhead of shifting and inspecting $default.
208        # Consequence: a single hashref always bypasses default key naming --
209        # documented in LIMITATIONS.
210
389
1187178
        return $_[0] if (@_ == 1) && (ref($_[0]) eq $T_HASH);
211
212
374
265
        my $default = shift;
213
214
374
400
        if (ref($default) && (ref($default) ne $T_ARRAY)) {
215
8
28
                Carp::croak(__PACKAGE__, '::get_params: $default must be a scalar or arrayref');
216        }
217
218        # Positional-names feature: $default is an arrayref of key names and the
219        # remaining @_ are values to map to those keys in order.
220
367
435
        if($default && (ref($default) eq $T_ARRAY)) {
221                # Honour the single-hashref passthrough for consistency with scalar $default.
222
32
51
                return $_[0] if (@_ == 1) && (ref($_[0]) eq $T_HASH);
223
28
29
                my %rc;
224
11
11
11
28
28
28
28
32
17
3190
18
20
65
32
                { no warnings 'uninitialized'; @rc{@{$default}} = @_[0 .. $#{$default}] }
225
28
46
                return \%rc;
226        }
227
228        # Detect \@_ usage: caller passed a reference to its own @_.
229
335
185
        my ($args, $from_arrayref);
230
335
474
        if ((@_ == 1) && (ref($_[0]) eq $T_ARRAY)) {
231                # Two-element shorthand: caller did routine('key' => 'scalar') and the
232                # callee received \@_.  Only fires when the value is a plain scalar to
233                # avoid ambiguity with an arrayref value.
234
143
47
124
91
                if($default && (@{$_[0]} == 2) && ($_[0]->[0] eq $default) && !ref($_[0]->[1])) {
235
10
19
                        return { $default => $_[0]->[1] };
236                }
237
133
70
                $args = $_[0];
238
133
85
                $from_arrayref = 1;
239        } else {
240
192
139
                $args = \@_;
241        }
242
243
325
325
179
209
        my $num_args = scalar @{$args};
244
245        # --- Zero arguments ---
246
325
242
        if ($num_args == 0) {
247
24
27
                if (defined $default) {
248                        # Full stack trace via Devel::Confess because receiving zero args
249                        # when a default is defined is virtually always a programming error.
250
14
53
                        Carp::confess('Usage: ', __PACKAGE__, '->', (caller(1))[3], "($default => \$val)");
251                }
252
11
20
                return;
253        }
254
255        # --- One argument ---
256
301
244
        if ($num_args == 1) {
257
129
100
                if (defined $default) {
258
73
53
                        my $arg  = $args->[0];
259
73
51
                        my $kind = ref($arg);
260
261
73
114
                        return { $default => $arg     } if !$kind;
262
38
34
                        return { $default => $arg     } if $kind eq $T_ARRAY;
263
36
9
38
19
                        return { $default => ${$arg}  } if $kind eq $T_SCALAR;
264
27
38
                        return { $default => $arg     } if $kind eq $T_CODE;
265
20
39
                        return { $default => $arg     } if Scalar::Util::blessed($arg);
266                        # Unblessed HASH ref falls through to the no-default path below,
267                        # where it is returned directly (see LIMITATIONS).
268                }
269
270
67
83
                return unless defined $args->[0];
271
272                # Copy before type checks: $args->[0] is an alias to the caller's
273                # variable via @_ -- assigning through it would silently mutate the
274                # caller's data.  Work on a named copy instead.
275
63
43
                my $val = $args->[0];
276
63
12
58
11
                $val = ${$val} if ref($val) eq $T_REF;
277
278
63
90
                return $val if ref($val) eq $T_HASH;
279
280                # Empty arrayref with no default: return the ref itself.
281
26
8
46
9
                if ((ref($val) eq $T_ARRAY) && (@{$val} == 0)) {
282
4
5
                        return $val;
283                }
284
285
22
41
                Carp::croak('Usage: ', __PACKAGE__, '->', (caller(1))[3], '()');
286        }
287
288        # --- Two arguments where the second is a hash ref ---
289        # Handles the Obj->new($mandatory, \%options) convention.
290
173
227
        if (($num_args == 2) && (ref($args->[1]) eq $T_HASH)) {
291
31
27
                if (defined $default) {
292
27
27
18
45
                        if (scalar keys %{$args->[1]}) {
293                                # When first arg is the default key name itself, second arg is its value.
294
23
34
                                return { $default => $args->[1] } if $args->[0] eq $default;
295                                # Otherwise: first arg is the mandatory value; options are merged.
296
18
18
16
44
                                return { $default => $args->[0], %{$args->[1]} };
297                        }
298                        # Empty options hashref: store the ref as the value.
299
4
9
                        return { $default => $args->[1] };
300                }
301        }
302
303        # --- \@_ with multiple values under a scalar default ---
304
146
190
        return { $default => $args } if $from_arrayref && defined $default;
305
306        # --- Even-length list: flat key/value pairs ---
307
132
124
        if (($num_args % 2) == 0) {
308
118
118
55
299
                my %rc = @{$args};
309
118
169
                return \%rc;
310        }
311
312
14
22
        Carp::croak('Usage: ', __PACKAGE__, '->', (caller(1))[3], '()');
313}
314
315 - 435
=head1 LIMITATIONS

=over 4

=item B<Single empty arrayref cannot be distinguished from C<\@_> of an empty list>

When the caller does C<foo([])> and the callee uses C<get_params('key', @_)>,
the C<@_> list is C<([])> -- one element, an arrayref.  The function
interprets the lone arrayref as a C<\@_> passthrough, unwraps it to an empty
list, and then croaks because C<$default> is defined but there are zero
arguments.  Workaround: pass the value as a named pair
(C<key =E<gt> []>) or ensure the callee always uses C<\@_>.

=item B<Single hash ref always bypasses C<$default> key naming>

C<get_params('config', { a =E<gt> 1 })> returns C<{ a =E<gt> 1 }>, not
C<{ config =E<gt> { a =E<gt> 1 } }>.  The fast path fires before C<$default>
is inspected.  To store a hash ref under a default key, pass it as a named
pair: C<get_params('config', config =E<gt> { a =E<gt> 1 })>.

=item B<No mechanism to mark a C<$default> argument as optional>

When C<$default> is a string and zero arguments are received, the function
always confesses.  There is no way to express I<"accept zero args and return
undef gracefully">.

=item B<Duplicate keys in a flat list silently overwrite; last value wins>

C<get_params(undef, foo =E<gt> 1, foo =E<gt> 2)> returns C<{ foo =E<gt> 2 }>
with no warning.  If an attacker controls part of the argument list, a
later duplicate key can silently override an earlier sanitised value.
Detect and reject duplicate keys in the validation layer (e.g.
L<Params::Validate::Strict>) rather than relying on C<get_params> to catch
them.

=item B<Positional-names C<$default> silently discards extra arguments>

C<get_params([qw(a b)], 1, 2, 3)> returns C<{ a =E<gt> 1, b =E<gt> 2 }>
and ignores C<3>.  If strict arity is required, validate the returned hash
with L<Params::Validate::Strict>.

=back

=head1 AUTHOR

Nigel Horne, C<< <njh at nigelhorne.com> >>

=head1 BUGS

Please report bugs or feature requests to C<bug-params-get at rt.cpan.org>
or through L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Params-Get>.

=head1 SEE ALSO

=over 4

=item * L<Params::Smart>

=item * L<Params::Validate::Strict>

=item * L<Return::Set>

=item * L<Test Dashboard|https://nigelhorne.github.io/Params-Get/coverage/>

=back

=head1 SUPPORT

=over 4

=item * MetaCPAN: L<https://metacpan.org/dist/Params-Get>

=item * RT: L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Params-Get>

=item * CPAN Testers: L<http://matrix.cpantesters.org/?dist=Params-Get>

=item * CPAN Testers Dependencies: L<http://deps.cpantesters.org/?module=Params::Get>

=back

=head2 FORMAL SPECIFICATION

=head3 get_params

    Let D = default key (Str | [Str*] | undef), A = argument tuple.

    get_params : D x A* -> HashRef | Undef

    -- Fast path (fires before D is inspected -- see LIMITATIONS)
    get_params(D, h)            == h             when |A|=1, h:HashRef

    -- Positional-names default
    get_params([n1..nk], v*)    == {ni -> vi}    i in 1..k, vi = undef when missing

    -- Scalar default, single arg
    get_params(d, s)            == {d -> s}      d:Str, s:Scalar
    get_params(d, a)            == {d -> a}      d:Str, a:ArrayRef
    get_params(d, \s)           == {d -> s}      d:Str (scalarref dereferenced)
    get_params(d, c)            == {d -> c}      d:Str, c:CodeRef
    get_params(d, o)            == {d -> o}      d:Str, o:BlessedObject

    -- Mandatory-positional + options-hashref
    get_params(d, v, {k->w..})  == {d->v, k->w..}    non-empty opts
    get_params(d, d, {k->w..})  == {d -> {k->w..}}   first arg IS the key name

    -- Named pairs
    get_params(undef, k1,v1..)  == {ki -> vi}    when |A| is even

    -- Empty / error
    get_params(undef)           == undef
    get_params(d)               => confess       d:Str (missing required arg)
    get_params(D, odd-list)     => croak

=head1 LICENCE AND COPYRIGHT

Copyright 2025-2026 Nigel Horne.

Usage is subject to the GPL2 licence terms.  If you use this module,
please let me know.

=cut
436
4371;