TER1 (Statement): 100.00%
TER2 (Branch): 100.00%
TER3 (LCSAJ): 100.0% (7/7)
Approximate LCSAJ segments: 51
● 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.
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;