lib/Params/Get.pm

Structural Coverage (Approximate)

TER1 (Statement): 100.00%
TER2 (Branch): 100.00%
TER3 (LCSAJ): 100.0% (7/7)
Approximate LCSAJ segments: 51

LCSAJ Legend

Covered — this LCSAJ path was executed during testing.

Not covered — this LCSAJ path was never executed. These are the paths to focus on.

Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:

        start → end → jump
        

Uncovered paths show [NOT COVERED] in the tooltip.

Mutant Testing Legend

Survived (tests missed this) Killed (tests detected this) No mutation
    1: package 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: use strict;
    8: use warnings;
    9: use autodie qw(:all);
   10: 
   11: use parent 'Exporter';
   12: 
   13: use Carp ();
   14: use Scalar::Util ();
   15: 
   16: use Readonly;
   17: 
   18: our @EXPORT_OK = qw(get_params);
   19: 
   20: =head1 NAME
   21: 
   22: Params::Get - Normalise subroutine arguments regardless of calling convention
   23: 
   24: =head1 VERSION
   25: 
   26: Version 0.16
   27: 
   28: =cut
   29: 
   30: our $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.
   34: Readonly::Scalar my $T_HASH   => 'HASH';
   35: Readonly::Scalar my $T_ARRAY  => 'ARRAY';
   36: Readonly::Scalar my $T_SCALAR => 'SCALAR';
   37: Readonly::Scalar my $T_CODE   => 'CODE';
   38: Readonly::Scalar my $T_REF    => 'REF';
   39: 
   40: =head1 DESCRIPTION
   41: 
   42: C<Params::Get> exports a single function, C<get_params>, which accepts a
   43: caller's argument list (or a reference to it) in any of the common Perl
   44: calling conventions and returns a unified hash-ref.  Library authors can
   45: write one normalisation call at the top of every public method rather than
   46: hand-rolling the same conditional chains in each one.
   47: 
   48: When combined with L<Params::Validate::Strict> and L<Return::Set> you can
   49: formally specify and enforce the input and output contracts of every method.
   50: 
   51: =head1 SYNOPSIS
   52: 
   53:     use Params::Get qw(get_params);
   54:     use Params::Validate::Strict;
   55: 
   56:     sub where_am_i {
   57:         my $params = Params::Validate::Strict::validate_strict({
   58:             args   => get_params(undef, \@_),
   59:             schema => {
   60:                 latitude  => { type => 'number', min => -90,  max =>  90 },
   61:                 longitude => { type => 'number', min => -180, max => 180 },
   62:             },
   63:         });
   64:         printf "You are at %s, %s\n",
   65:             $params->{latitude}, $params->{longitude};
   66:     }
   67: 
   68:     where_am_i(latitude => 0.3, longitude => 124);
   69:     where_am_i({ latitude => 3.14, longitude => -155 });
   70: 
   71: =head1 METHODS
   72: 
   73: =head2 get_params
   74: 
   75: Parse the argument list passed to a subroutine and return a unified hash-ref
   76: regardless of the calling convention used.  Supported conventions:
   77: 
   78: =over 4
   79: 
   80: =item * Single hash-ref: C<foo({ a =E<gt> 1 })>
   81: 
   82: =item * Named key/value pairs: C<foo(a =E<gt> 1, b =E<gt> 2)>
   83: 
   84: =item * Single scalar with a default key: C<foo('US')> given C<get_params('country', @_)>
   85: 
   86: =item * Array-ref shorthand: C<foo(\@_)> inside the callee
   87: 
   88: =item * Mandatory positional argument plus an options hash-ref:
   89: C<Obj-E<gt>new($val, { opt =E<gt> 1 })>
   90: 
   91: =item * Scalar-ref: C<foo(\'text')> -- dereferenced automatically
   92: 
   93: =item * Blessed object or CODE ref: mapped under C<$default>
   94: 
   95: =item * Array-ref of positional key names as C<$default>:
   96: C<get_params([qw(name age)], @_)>
   97: 
   98: =back
   99: 
  100: =head3 ARGUMENTS
  101: 
  102: =over 4
  103: 
  104: =item C<$default> (scalar string, arrayref of strings, or C<undef>)
  105: 
  106: Controls how a single non-hash argument is interpreted:
  107: 
  108: =over 8
  109: 
  110: =item * B<string> -- used as the key name when a lone scalar, ref, or object
  111: is received.
  112: 
  113: =item * B<arrayref of strings> -- positional key names; the I<n>th argument
  114: is mapped to the I<n>th name.  Extra arguments are silently discarded;
  115: missing arguments produce C<undef> values.
  116: 
  117: =item * B<undef> -- no default key; the caller must pass named pairs or a
  118: single hash-ref.  An empty argument list returns C<undef>.
  119: 
  120: =back
  121: 
  122: =item C<@args>
  123: 
  124: The caller's argument list, passed either as a flat list (C<@_>) or as a
  125: reference to the array (C<\@_>).  Both forms are accepted transparently.
  126: 
  127: =back
  128: 
  129: =head3 RETURNS
  130: 
  131: A hash-ref on success, or C<undef> when C<$default> is C<undef> and no
  132: arguments are provided.
  133: 
  134: =head3 SIDE EFFECTS
  135: 
  136: Croaks from the caller's frame on programming errors (wrong calling
  137: convention, non-ARRAY ref passed as C<$default>).  Confesses with a full
  138: stack trace when C<$default> is defined but zero arguments are received,
  139: because that almost always indicates a programming error.
  140: 
  141: =head3 API SPECIFICATION
  142: 
  143: =head4 Input
  144: 
  145: 	{
  146: 		default => {
  147: 			type => [ 'string', 'stringref' ],
  148: 			optional => 1,
  149: 			position => 0,
  150: 		}, args => {
  151: 			type => [ 'array', 'arrayref' ],
  152: 			optional => 1,
  153: 			position => 1,
  154: 		}
  155: 	}
  156: 
  157: =head4 output
  158: 
  159:     {
  160: 	type => 'hashref',
  161: 	optional => 1,
  162:     }
  163: 
  164: =head3 MESSAGES
  165: 
  166:     Message                                             Meaning                                Resolution
  167:     --------------------------------------------------  -------------------------------------  ----------------------------------------------
  168:     ::get_params: $default must be a scalar or          A non-ARRAY ref was passed as          Pass a plain string, arrayref of strings,
  169:       arrayref                                          $default                               or undef
  170:     Usage: Pkg->method($key => $val)  [stack trace]    $default is defined but no args given  Ensure the caller always passes a value
  171:     Usage: Pkg->method()                               Odd-length or unrecognisable arg list  Correct the calling convention in the caller
  172: 
  173: =head3 PSEUDOCODE
  174: 
  175:     1.  Fast-path: if the sole argument is a plain HASH ref, return it
  176:         immediately (fires before $default is inspected -- see LIMITATIONS).
  177: 
  178:     2.  Shift $default.  Validate: must be undef, a plain scalar, or an
  179:         ARRAY ref.  Any other ref type croaks immediately.
  180: 
  181:     3.  If $default is an ARRAY ref, map remaining @_ positionally to those
  182:         key names and return.  A single plain HASH ref is still passed
  183:         through unchanged.
  184: 
  185:     4.  Detect the \@_ calling convention: if exactly one ARRAY ref argument
  186:         remains, check the two-element (key => scalar-val) shorthand and
  187:         return immediately when it matches.  Otherwise unwrap and use the
  188:         array contents as the effective @args.
  189: 
  190:     5.  Dispatch on argument count:
  191:         0 -- confess (with stack trace) if $default is defined;
  192:              return undef otherwise.
  193:         1 -- if $default is defined, wrap the single arg under $default
  194:              (scalar, arrayref, scalarref->deref, coderef, blessed object).
  195:              Without $default: unwrap REF-of-REF, pass HASH ref through,
  196:              return empty ARRAY ref as-is.  Anything else: croak.
  197:         2 with HASH ref as arg[1]
  198:           -- Mandatory-positional + options-hashref pattern.
  199:         even N -- treat as flat key/value pairs.
  200:         odd N  -- croak.
  201: 
  202: =cut
  203: 
  204: sub 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 → 214 → 220  210: 	return $_[0] if (@_ == 1) && (ref($_[0]) eq $T_HASH);

Mutants (Total: 3, Killed: 3, Survived: 0)

211: 212: my $default = shift; 213: 214: if (ref($default) && (ref($default) ne $T_ARRAY)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

215: 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 → 220 → 229 220: if($default && (ref($default) eq $T_ARRAY)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

221: # Honour the single-hashref passthrough for consistency with scalar $default. 222: return $_[0] if (@_ == 1) && (ref($_[0]) eq $T_HASH);

Mutants (Total: 3, Killed: 3, Survived: 0)

223: my %rc; 224: { no warnings 'uninitialized'; @rc{@{$default}} = @_[0 .. $#{$default}] } 225: return \%rc;

Mutants (Total: 2, Killed: 2, Survived: 0)

226: } 227: 228: # Detect \@_ usage: caller passed a reference to its own @_. 229 → 230 → 243 229: my ($args, $from_arrayref); 230: if ((@_ == 1) && (ref($_[0]) eq $T_ARRAY)) {

Mutants (Total: 2, Killed: 2, Survived: 0)

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: if($default && (@{$_[0]} == 2) && ($_[0]->[0] eq $default) && !ref($_[0]->[1])) {

Mutants (Total: 2, Killed: 2, Survived: 0)

235: return { $default => $_[0]->[1] }; 236: } 237: $args = $_[0]; 238: $from_arrayref = 1; 239: } else { 240: $args = \@_; 241: } 242: 243 → 246 → 256 243: my $num_args = scalar @{$args}; 244: 245: # --- Zero arguments --- 246: if ($num_args == 0) {

Mutants (Total: 2, Killed: 2, Survived: 0)

247: if (defined $default) {

Mutants (Total: 1, Killed: 1, Survived: 0)

248: # Full stack trace via Devel::Confess because receiving zero args 249: # when a default is defined is virtually always a programming error. 250: Carp::confess('Usage: ', __PACKAGE__, '->', (caller(1))[3], "($default => \$val)"); 251: } 252: return; 253: } 254: 255: # --- One argument --- 256 → 256 → 290 256: if ($num_args == 1) {

Mutants (Total: 2, Killed: 2, Survived: 0)

257: if (defined $default) {

Mutants (Total: 1, Killed: 1, Survived: 0)

258: my $arg = $args->[0]; 259: my $kind = ref($arg); 260: 261: return { $default => $arg } if !$kind; 262: return { $default => $arg } if $kind eq $T_ARRAY; 263: return { $default => ${$arg} } if $kind eq $T_SCALAR; 264: return { $default => $arg } if $kind eq $T_CODE; 265: 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: 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: my $val = $args->[0]; 276: $val = ${$val} if ref($val) eq $T_REF; 277: 278: return $val if ref($val) eq $T_HASH;

Mutants (Total: 2, Killed: 2, Survived: 0)

279: 280: # Empty arrayref with no default: return the ref itself. 281: if ((ref($val) eq $T_ARRAY) && (@{$val} == 0)) {

Mutants (Total: 2, Killed: 2, Survived: 0)

282: return $val;

Mutants (Total: 2, Killed: 2, Survived: 0)

283: } 284: 285: 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 → 290 → 304 290: if (($num_args == 2) && (ref($args->[1]) eq $T_HASH)) {

Mutants (Total: 2, Killed: 2, Survived: 0)

291: if (defined $default) {

Mutants (Total: 1, Killed: 1, Survived: 0)

292: if (scalar keys %{$args->[1]}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

293: # When first arg is the default key name itself, second arg is its value. 294: return { $default => $args->[1] } if $args->[0] eq $default; 295: # Otherwise: first arg is the mandatory value; options are merged. 296: return { $default => $args->[0], %{$args->[1]} }; 297: } 298: # Empty options hashref: store the ref as the value. 299: return { $default => $args->[1] }; 300: } 301: } 302: 303: # --- \@_ with multiple values under a scalar default --- 304 → 307 → 312 304: return { $default => $args } if $from_arrayref && defined $default; 305: 306: # --- Even-length list: flat key/value pairs --- 307: if (($num_args % 2) == 0) {

Mutants (Total: 2, Killed: 2, Survived: 0)

308: my %rc = @{$args}; 309: return \%rc;

Mutants (Total: 2, Killed: 2, Survived: 0)

310: } 311: 312: Carp::croak('Usage: ', __PACKAGE__, '->', (caller(1))[3], '()'); 313: } 314: 315: =head1 LIMITATIONS 316: 317: =over 4 318: 319: =item B<Single empty arrayref cannot be distinguished from C<\@_> of an empty list> 320: 321: When the caller does C<foo([])> and the callee uses C<get_params('key', @_)>, 322: the C<@_> list is C<([])> -- one element, an arrayref. The function 323: interprets the lone arrayref as a C<\@_> passthrough, unwraps it to an empty 324: list, and then croaks because C<$default> is defined but there are zero 325: arguments. Workaround: pass the value as a named pair 326: (C<key =E<gt> []>) or ensure the callee always uses C<\@_>. 327: 328: =item B<Single hash ref always bypasses C<$default> key naming> 329: 330: C<get_params('config', { a =E<gt> 1 })> returns C<{ a =E<gt> 1 }>, not 331: C<{ config =E<gt> { a =E<gt> 1 } }>. The fast path fires before C<$default> 332: is inspected. To store a hash ref under a default key, pass it as a named 333: pair: C<get_params('config', config =E<gt> { a =E<gt> 1 })>. 334: 335: =item B<No mechanism to mark a C<$default> argument as optional> 336: 337: When C<$default> is a string and zero arguments are received, the function 338: always confesses. There is no way to express I<"accept zero args and return 339: undef gracefully">. 340: 341: =item B<Duplicate keys in a flat list silently overwrite; last value wins> 342: 343: C<get_params(undef, foo =E<gt> 1, foo =E<gt> 2)> returns C<{ foo =E<gt> 2 }> 344: with no warning. If an attacker controls part of the argument list, a 345: later duplicate key can silently override an earlier sanitised value. 346: Detect and reject duplicate keys in the validation layer (e.g. 347: L<Params::Validate::Strict>) rather than relying on C<get_params> to catch 348: them. 349: 350: =item B<Positional-names C<$default> silently discards extra arguments> 351: 352: C<get_params([qw(a b)], 1, 2, 3)> returns C<{ a =E<gt> 1, b =E<gt> 2 }> 353: and ignores C<3>. If strict arity is required, validate the returned hash 354: with L<Params::Validate::Strict>. 355: 356: =back 357: 358: =head1 AUTHOR 359: 360: Nigel Horne, C<< <njh at nigelhorne.com> >> 361: 362: =head1 BUGS 363: 364: Please report bugs or feature requests to C<bug-params-get at rt.cpan.org> 365: or through L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Params-Get>. 366: 367: =head1 SEE ALSO 368: 369: =over 4 370: 371: =item * L<Params::Smart> 372: 373: =item * L<Params::Validate::Strict> 374: 375: =item * L<Return::Set> 376: 377: =item * L<Test Dashboard|https://nigelhorne.github.io/Params-Get/coverage/> 378: 379: =back 380: 381: =head1 SUPPORT 382: 383: =over 4 384: 385: =item * MetaCPAN: L<https://metacpan.org/dist/Params-Get> 386: 387: =item * RT: L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Params-Get> 388: 389: =item * CPAN Testers: L<http://matrix.cpantesters.org/?dist=Params-Get> 390: 391: =item * CPAN Testers Dependencies: L<http://deps.cpantesters.org/?module=Params::Get> 392: 393: =back 394: 395: =head2 FORMAL SPECIFICATION 396: 397: =head3 get_params 398: 399: Let D = default key (Str | [Str*] | undef), A = argument tuple. 400: 401: get_params : D x A* -> HashRef | Undef 402: 403: -- Fast path (fires before D is inspected -- see LIMITATIONS) 404: get_params(D, h) == h when |A|=1, h:HashRef 405: 406: -- Positional-names default 407: get_params([n1..nk], v*) == {ni -> vi} i in 1..k, vi = undef when missing 408: 409: -- Scalar default, single arg 410: get_params(d, s) == {d -> s} d:Str, s:Scalar 411: get_params(d, a) == {d -> a} d:Str, a:ArrayRef 412: get_params(d, \s) == {d -> s} d:Str (scalarref dereferenced) 413: get_params(d, c) == {d -> c} d:Str, c:CodeRef 414: get_params(d, o) == {d -> o} d:Str, o:BlessedObject 415: 416: -- Mandatory-positional + options-hashref 417: get_params(d, v, {k->w..}) == {d->v, k->w..} non-empty opts 418: get_params(d, d, {k->w..}) == {d -> {k->w..}} first arg IS the key name 419: 420: -- Named pairs 421: get_params(undef, k1,v1..) == {ki -> vi} when |A| is even 422: 423: -- Empty / error 424: get_params(undef) == undef 425: get_params(d) => confess d:Str (missing required arg) 426: get_params(D, odd-list) => croak 427: 428: =head1 LICENCE AND COPYRIGHT 429: 430: Copyright 2025-2026 Nigel Horne. 431: 432: Usage is subject to the GPL2 licence terms. If you use this module, 433: please let me know. 434: 435: =cut 436: 437: 1;