TER1 (Statement): 100.00%
TER2 (Branch): 100.00%
TER3 (LCSAJ): 100.0% (7/7)
Approximate LCSAJ segments: 45
● 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: # TODO: Investigate Params::Smart 8: 9: use strict; 10: use warnings; 11: use autodie qw(:all); 12: 13: use parent 'Exporter'; 14: 15: use Carp (); 16: use Scalar::Util (); 17: 18: use Readonly; 19: 20: our @EXPORT_OK = qw(get_params); 21: 22: =head1 NAME 23: 24: Params::Get - Normalise subroutine arguments regardless of calling convention 25: 26: =head1 VERSION 27: 28: Version 0.17 29: 30: =cut 31: 32: our $VERSION = '0.17'; 33: 34: # Reference-type sentinels. Collected here so a typo is a compile-time 35: # error via Readonly and grep/ack finds every usage in one search. 36: Readonly::Scalar my $T_HASH => 'HASH'; 37: Readonly::Scalar my $T_ARRAY => 'ARRAY'; 38: Readonly::Scalar my $T_SCALAR => 'SCALAR'; 39: Readonly::Scalar my $T_CODE => 'CODE'; 40: Readonly::Scalar my $T_REF => 'REF'; 41: 42: =head1 DESCRIPTION 43: 44: C<Params::Get> exports a single function, C<get_params>, which accepts a 45: caller's argument list (or a reference to it) in any of the common Perl 46: calling conventions and returns a unified hash-ref. Library authors can 47: write one normalisation call at the top of every public method rather than 48: hand-rolling the same conditional chains in each one. 49: 50: When combined with L<Params::Validate::Strict> and L<Return::Set> you can 51: formally specify and enforce the input and output contracts of every method. 52: 53: =head1 SYNOPSIS 54: 55: use Params::Get qw(get_params); 56: use Params::Validate::Strict; 57: 58: sub where_am_i { 59: my $params = Params::Validate::Strict::validate_strict({ 60: args => get_params(undef, \@_), 61: schema => { 62: latitude => { type => 'number', min => -90, max => 90 }, 63: longitude => { type => 'number', min => -180, max => 180 }, 64: }, 65: }); 66: printf "You are at %s, %s\n", 67: $params->{latitude}, $params->{longitude}; 68: } 69: 70: where_am_i(latitude => 0.3, longitude => 124); 71: where_am_i({ latitude => 3.14, longitude => -155 }); 72: 73: =head1 METHODS 74: 75: =head2 get_params 76: 77: Parse the argument list passed to a subroutine and return a unified hash-ref 78: regardless of the calling convention used. Supported conventions: 79: 80: =over 4 81: 82: =item * Single hash-ref: C<foo({ a =E<gt> 1 })> 83: 84: =item * Named key/value pairs: C<foo(a =E<gt> 1, b =E<gt> 2)> 85: 86: =item * Single scalar with a default key: C<foo('US')> given C<get_params('country', @_)> 87: 88: =item * Array-ref shorthand: C<foo(\@_)> inside the callee 89: 90: =item * Mandatory positional argument plus an options hash-ref: 91: C<Obj-E<gt>new($val, { opt =E<gt> 1 })> 92: 93: =item * Scalar-ref: C<foo(\'text')> -- dereferenced automatically 94: 95: =item * Blessed object or CODE ref: mapped under C<$default> 96: 97: =item * Array-ref of positional key names as C<$default>: 98: C<get_params([qw(name age)], @_)> 99: 100: =back 101: 102: =head3 ARGUMENTS 103: 104: =over 4 105: 106: =item C<$default> (scalar string, arrayref of strings, or C<undef>) 107: 108: Controls how a single non-hash argument is interpreted: 109: 110: =over 8 111: 112: =item * B<string> -- used as the key name when a lone scalar, ref, or object 113: is received. 114: 115: =item * B<arrayref of strings> -- positional key names; the I<n>th argument 116: is mapped to the I<n>th name. Extra arguments are silently discarded; 117: missing arguments produce C<undef> values. 118: 119: =item * B<undef> -- no default key; the caller must pass named pairs or a 120: single hash-ref. An empty argument list returns C<undef>. 121: 122: =back 123: 124: =item C<@args> 125: 126: The caller's argument list, passed either as a flat list (C<@_>) or as a 127: reference to the array (C<\@_>). Both forms are accepted transparently. 128: 129: =back 130: 131: =head3 RETURNS 132: 133: A hash-ref on success, or C<undef> when C<$default> is C<undef> and no 134: arguments are provided. 135: 136: =head3 SIDE EFFECTS 137: 138: Croaks from the caller's frame on programming errors (wrong calling 139: convention, non-ARRAY ref passed as C<$default>). Confesses with a full 140: stack trace when C<$default> is defined but zero arguments are received, 141: because that almost always indicates a programming error. 142: 143: =head3 API SPECIFICATION 144: 145: =head4 Input 146: 147: { 148: default => { 149: type => [ 'string', 'stringref' ], 150: optional => 1, 151: position => 0, 152: }, args => { 153: type => [ 'array', 'arrayref' ], 154: optional => 1, 155: position => 1, 156: } 157: } 158: 159: =head4 Domain Partitions and Boundary Values 160: 161: B<C<$default> -- position 0> 162: 163: Valid partitions: 164: EP1 undef No default key; caller must pass named pairs or a hashref. 165: EP2 non-empty str Used as the hash key for a single positional argument. 166: Truthy: the two-element \@_ shorthand is active. 167: EP3 "" Valid but FALSY; the shorthand guard ($default && ...) 168: is suppressed. Named-pairs path is used instead. 169: EP4 "0" Valid but FALSY; identical behaviour to EP3. 170: EP5 ARRAY ref Positional-names mode: nth arg maps to nth key name. 171: EP6 [] Valid; 0 named slots -- all positional args discarded. 172: 173: Invalid partitions (croak at guard, before @args is inspected): 174: EP7 CODE ref 175: EP8 HASH ref 176: EP9 SCALAR ref 177: EP10 REF (ref-of-ref) 178: EP11 Blessed non-ARRAY object (ref() returns class name, not expected type) 179: EP12 Blessed ARRAY object (ref() returns class name, not "ARRAY") 180: 181: BVA edges for string $default: 182: length=0 ("") Valid, falsy, opaque key. 183: length=1 Minimum truthy string; shorthand guard active. 184: length=65536 Maximum tested; accepted without error. 185: 186: BVA edges for ARRAY ref $default key count: 187: 0 keys All positional args discarded; {} returned. 188: 1 key Only first arg mapped. 189: keys > @args Missing args produce undef-valued slots (no warning). 190: keys < @args Extra args silently discarded. 191: 192: B<C<@args> -- position 1..N> 193: 194: Valid partitions: 195: CN0a 0 args, EP1 ($default undef) Returns undef. 196: CN1 1 HASH ref, no $default Fast path; hashref returned by identity. 197: CN2 1 non-HASH, defined $default Arg wrapped under $default key. 198: CN3 2 args, 2nd HASH, defined $d OO constructor path; see below. 199: CN4 even N >= 2, no CN3 match Flat key/value pairs. 200: 201: Invalid partitions: 202: CN0b 0 args, defined $default Carp::confess with full stack trace. 203: CN5 odd N >= 3 Carp::croak with usage message. 204: 205: BVA edges for @args count: 206: 0 Minimum; routes to CN0a or CN0b. 207: 1 Single-arg dispatch; many sub-partitions by arg type (see below). 208: 2 Even minimum; triggers CN3 when 2nd arg is a HASH ref. 209: 3 Minimum odd; triggers CN5 (croak). 210: 4 Minimum even for CN4 (plain pairs). 211: 1000 Maximum tested; accepted in O(n). 212: 213: B<OO constructor options hash -- key-count BVA> 214: 215: When C<$num_args == 2> and C<ref($args-E<gt>[1]) eq 'HASH'>: 216: 217: keys=0 Empty hashref stored as value: { $default => {} }. 218: keys=1 Non-empty; if first arg IS $default key name: hashref wrapped. 219: Otherwise: first arg is mandatory value; options merged in. 220: keys=N Same as keys=1 non-match case; all options merged. 221: 222: B<Single-arg type sub-partitions (with defined C<$default>)> 223: 224: undef Wrapped: { $default => undef } 225: "" Wrapped: { $default => "" } 226: "0" Wrapped: { $default => "0" } 227: string Wrapped as-is. 228: SCALAR ref Dereferenced then wrapped: { $default => ${$arg} }. 229: ARRAY ref Wrapped as-is (not dereferenced). 230: CODE ref Wrapped as-is. 231: blessed object Wrapped as-is. 232: HASH ref LIMITATION: bypasses $default; returned by identity. 233: 234: =head4 output 235: 236: { 237: type => 'hashref', 238: optional => 1, 239: } 240: 241: =head3 MESSAGES 242: 243: Message Meaning Resolution 244: -------------------------------------------------- ------------------------------------- ---------------------------------------------- 245: ::get_params: $default must be a scalar or A non-ARRAY ref was passed as Pass a plain string, arrayref of strings, 246: arrayref $default or undef 247: Usage: Pkg->method($key => $val) [stack trace] $default is defined but no args given Ensure the caller always passes a value 248: Usage: Pkg->method() Odd-length or unrecognisable arg list Correct the calling convention in the caller 249: 250: =head3 PSEUDOCODE 251: 252: 1. Fast-path: if the sole argument is a plain HASH ref, return it 253: immediately (fires before $default is inspected -- see LIMITATIONS). 254: 255: 2. Shift $default. Validate: must be undef, a plain scalar, or an 256: ARRAY ref. Any other ref type croaks immediately. 257: 258: 3. If $default_ref eq "ARRAY", map remaining @_ positionally to the key 259: names and return. (Premise: an ARRAY ref is always truthy, so no 260: separate truthiness pre-check is needed -- $default_ref eq "ARRAY" 261: is sufficient.) 262: 263: 4. Detect the \@_ calling convention: if exactly one ARRAY ref argument 264: remains, check the two-element (key => scalar-val) shorthand and 265: return immediately when it matches. The shorthand guard uses 266: truthiness (not definedness) so that falsy $default strings ("0", "") 267: suppress it -- this is a documented invariant. Otherwise unwrap and 268: use the array contents as the effective @args. 269: 270: 5. Dispatch on argument count: 271: 0 -- confess (with stack trace) if $default is defined; 272: return undef otherwise. 273: 1 -- if $default is defined, two arms cover all wrappable types: 274: SCALAR ref -> deref then wrap (pulled left as fast guard). 275: plain scalar | ARRAY | CODE | blessed -> wrap as-is. 276: Unblessed HASH and exotic refs fall through to the no-default 277: path (see LIMITATIONS). 278: Without $default: unwrap REF-of-REF, pass HASH ref through, 279: return empty ARRAY ref as-is. Anything else: croak. 280: 2 with HASH ref as arg[1] 281: -- Mandatory-positional + options-hashref pattern. 282: even N -- treat as flat key/value pairs. 283: odd N -- croak. 284: 285: =cut 286: 287: sub get_params 288: { 289: # Fast path: sole argument is already a plain hashref. Returning it 290: # directly avoids the overhead of shifting and inspecting $default. 291: # Consequence: a single hashref always bypasses default key naming -- 292: # documented in LIMITATIONS. ●293 → 298 → 306 293: return $_[0] if (@_ == 1) && (ref($_[0]) eq $T_HASH);Mutants (Total: 3, Killed: 3, Survived: 0)
294: 295: my $default = shift; 296: my $default_ref = ref($default); 297: 298: if ($default_ref && ($default_ref ne $T_ARRAY)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
299: Carp::croak(__PACKAGE__, '::get_params: $default must be a scalar or arrayref'); 300: } 301: 302: # Positional-names feature: $default is an arrayref of key names and the 303: # remaining @_ are values to map to those keys in order. 304: # Premise: ref(X) eq "ARRAY" implies X is a reference, which is always truthy. 305: # Conclusion: the "$default &&" pre-check would never flip this branch; omitted. ●306 → 306 → 315 306: if($default_ref eq $T_ARRAY) {
Mutants (Total: 1, Killed: 1, Survived: 0)
307: # Honour the single-hashref passthrough for consistency with scalar $default. 308: return $_[0] if (@_ == 1) && (ref($_[0]) eq $T_HASH);
Mutants (Total: 3, Killed: 3, Survived: 0)
309: my %rc; 310: { no warnings 'uninitialized'; @rc{@{$default}} = @_[0 .. $#{$default}] } 311: return \%rc;
Mutants (Total: 2, Killed: 2, Survived: 0)
312: } 313: 314: # Detect \@_ usage: caller passed a reference to its own @_. ●315 → 316 → 329 315: my ($args, $from_arrayref); 316: if ((@_ == 1) && (ref($_[0]) eq $T_ARRAY)) {
Mutants (Total: 2, Killed: 2, Survived: 0)
317: # Two-element shorthand: caller did routine('key' => 'scalar') and the 318: # callee received \@_. Only fires when the value is a plain scalar to 319: # avoid ambiguity with an arrayref value. 320: if($default && (@{$_[0]} == 2) && ($_[0]->[0] eq $default) && !ref($_[0]->[1])) {
Mutants (Total: 2, Killed: 2, Survived: 0)
321: return { $default => $_[0]->[1] }; 322: } 323: $args = $_[0]; 324: $from_arrayref = 1; 325: } else { 326: $args = \@_; 327: } 328: ●329 → 332 → 342 329: my $num_args = scalar @{$args}; 330: 331: # --- Zero arguments --- 332: if ($num_args == 0) {
Mutants (Total: 2, Killed: 2, Survived: 0)
333: if (defined $default) {
Mutants (Total: 1, Killed: 1, Survived: 0)
334: # Full stack trace via Devel::Confess because receiving zero args 335: # when a default is defined is virtually always a programming error. 336: Carp::confess('Usage: ', __PACKAGE__, '->', (caller(1))[3], "($default => \$val)"); 337: } 338: return; 339: } 340: 341: # --- One argument --- ●342 → 342 → 381 342: if ($num_args == 1) {
Mutants (Total: 2, Killed: 2, Survived: 0)
343: if (defined $default) {
Mutants (Total: 1, Killed: 1, Survived: 0)
344: my $arg = $args->[0]; 345: my $kind = ref($arg); 346: 347: # SCALAR ref is the only arm with a distinct action (deref before wrap); 348: # pull it left as a fast guard (Modus Ponens / fail-fast). 349: return { $default => ${$arg} } if $kind eq $T_SCALAR; 350: 351: # De Morgan reduction: four arms with identical action collapse to one 352: # disjunction. Premise: !$kind (plain scalar), ARRAY, CODE, and blessed 353: # objects all return the arg as-is under $default. 354: # Conclusion: unblessed HASH / REF / exotic refs fall through to the 355: # no-default path below (see LIMITATIONS). 356: return { $default => $arg } 357: if !$kind || $kind eq $T_ARRAY || $kind eq $T_CODE 358: || Scalar::Util::blessed($arg); 359: } 360: 361: return unless defined $args->[0]; 362: 363: # Copy before type checks: $args->[0] is an alias to the caller's 364: # variable via @_ -- assigning through it would silently mutate the 365: # caller's data. Work on a named copy instead. 366: my $val = $args->[0]; 367: $val = ${$val} if ref($val) eq $T_REF; 368: 369: return $val if ref($val) eq $T_HASH;
Mutants (Total: 2, Killed: 2, Survived: 0)
370: 371: # Empty arrayref with no default: return the ref itself. 372: if ((ref($val) eq $T_ARRAY) && (@{$val} == 0)) {
Mutants (Total: 2, Killed: 2, Survived: 0)
373: return $val;
Mutants (Total: 2, Killed: 2, Survived: 0)
374: } 375: 376: Carp::croak('Usage: ', __PACKAGE__, '->', (caller(1))[3], '()'); 377: } 378: 379: # --- Two arguments where the second is a hash ref --- 380: # Handles the Obj->new($mandatory, \%options) convention. ●381 → 381 → 395 381: if (($num_args == 2) && (ref($args->[1]) eq $T_HASH)) {
Mutants (Total: 2, Killed: 2, Survived: 0)
382: if (defined $default) {
Mutants (Total: 1, Killed: 1, Survived: 0)
383: if (scalar keys %{$args->[1]}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
384: # When first arg is the default key name itself, second arg is its value. 385: return { $default => $args->[1] } if $args->[0] eq $default; 386: # Otherwise: first arg is the mandatory value; options are merged. 387: return { $default => $args->[0], %{$args->[1]} }; 388: } 389: # Empty options hashref: store the ref as the value. 390: return { $default => $args->[1] }; 391: } 392: } 393: 394: # --- \@_ with multiple values under a scalar default --- ●395 → 398 → 402 395: return { $default => $args } if $from_arrayref && defined $default; 396: 397: # --- Even-length list: flat key/value pairs --- 398: if (($num_args % 2) == 0) {
Mutants (Total: 2, Killed: 2, Survived: 0)
399: return { @{$args} }; 400: } 401: 402: Carp::croak('Usage: ', __PACKAGE__, '->', (caller(1))[3], '()'); 403: } 404: 405: =head1 LIMITATIONS 406: 407: =over 4 408: 409: =item B<Single empty arrayref cannot be distinguished from C<\@_> of an empty list> 410: 411: When the caller does C<foo([])> and the callee uses C<get_params('key', @_)>, 412: the C<@_> list is C<([])> -- one element, an arrayref. The function 413: interprets the lone arrayref as a C<\@_> passthrough, unwraps it to an empty 414: list, and then croaks because C<$default> is defined but there are zero 415: arguments. Workaround: pass the value as a named pair 416: (C<key =E<gt> []>) or ensure the callee always uses C<\@_>. 417: 418: =item B<Single hash ref always bypasses C<$default> key naming> 419: 420: C<get_params('config', { a =E<gt> 1 })> returns C<{ a =E<gt> 1 }>, not 421: C<{ config =E<gt> { a =E<gt> 1 } }>. The fast path fires before C<$default> 422: is inspected. To store a hash ref under a default key, pass it as a named 423: pair: C<get_params('config', config =E<gt> { a =E<gt> 1 })>. 424: 425: =item B<No mechanism to mark a C<$default> argument as optional> 426: 427: When C<$default> is a string and zero arguments are received, the function 428: always confesses. There is no way to express I<"accept zero args and return 429: undef gracefully">. 430: 431: =item B<Duplicate keys in a flat list silently overwrite; last value wins> 432: 433: C<get_params(undef, foo =E<gt> 1, foo =E<gt> 2)> returns C<{ foo =E<gt> 2 }> 434: with no warning. If an attacker controls part of the argument list, a 435: later duplicate key can silently override an earlier sanitised value. 436: Detect and reject duplicate keys in the validation layer (e.g. 437: L<Params::Validate::Strict>) rather than relying on C<get_params> to catch 438: them. 439: 440: =item B<Positional-names C<$default> silently discards extra arguments> 441: 442: C<get_params([qw(a b)], 1, 2, 3)> returns C<{ a =E<gt> 1, b =E<gt> 2 }> 443: and ignores C<3>. If strict arity is required, validate the returned hash 444: with L<Params::Validate::Strict>. 445: 446: =back 447: 448: =head1 AUTHOR 449: 450: Nigel Horne, C<< <njh at nigelhorne.com> >> 451: 452: =head1 BUGS 453: 454: Please report bugs or feature requests to C<bug-params-get at rt.cpan.org> 455: or through L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Params-Get>. 456: 457: =head1 SEE ALSO 458: 459: =over 4 460: 461: =item * L<Params::Smart> 462: 463: =item * L<Params::Validate::Strict> 464: 465: =item * L<Return::Set> 466: 467: =item * L<Test Dashboard|https://nigelhorne.github.io/Params-Get/coverage/> 468: 469: =back 470: 471: =head1 SUPPORT 472: 473: This module is provided as-is without any warranty. 474: 475: =over 4 476: 477: =item * MetaCPAN: L<https://metacpan.org/dist/Params-Get> 478: 479: =item * RT: L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Params-Get> 480: 481: =item * CPAN Testers: L<http://matrix.cpantesters.org/?dist=Params-Get> 482: 483: =item * CPAN Testers Dependencies: L<http://deps.cpantesters.org/?module=Params::Get> 484: 485: =back 486: 487: =head2 FORMAL SPECIFICATION 488: 489: =head3 get_params 490: 491: Let D = default key (Str | [Str*] | undef), A = argument tuple. 492: 493: get_params : D x A* -> HashRef | Undef 494: 495: -- Fast path (fires before D is inspected -- see LIMITATIONS) 496: get_params(D, h) == h when |A|=1, h:HashRef 497: 498: -- Positional-names default 499: get_params([n1..nk], v*) == {ni -> vi} i in 1..k, vi = undef when missing 500: 501: -- Scalar default, single arg 502: get_params(d, s) == {d -> s} d:Str, s:Scalar 503: get_params(d, a) == {d -> a} d:Str, a:ArrayRef 504: get_params(d, \s) == {d -> s} d:Str (scalarref dereferenced) 505: get_params(d, c) == {d -> c} d:Str, c:CodeRef 506: get_params(d, o) == {d -> o} d:Str, o:BlessedObject 507: 508: -- Mandatory-positional + options-hashref 509: get_params(d, v, {k->w..}) == {d->v, k->w..} non-empty opts 510: get_params(d, d, {k->w..}) == {d -> {k->w..}} first arg IS the key name 511: 512: -- Named pairs 513: get_params(undef, k1,v1..) == {ki -> vi} when |A| is even 514: 515: -- Empty / error 516: get_params(undef) == undef 517: get_params(d) => confess d:Str (missing required arg) 518: get_params(D, odd-list) => croak 519: 520: =head1 LICENCE AND COPYRIGHT 521: 522: Copyright 2025-2026 Nigel Horne. 523: 524: Usage is subject to the GPL2 licence terms. If you use this module, 525: please let me know. 526: 527: =cut 528: 529: 1;