TER1 (Statement): 83.94%
TER2 (Branch): 71.01%
TER3 (LCSAJ): 98.3% (57/58)
Approximate LCSAJ segments: 277
โ 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 Config::Abstraction; 2: 3: # TODO: add TOML file support 4: # TODO: environment-specific encodings - automatic loading of dev/staging/prod 5: # TODO: devise a scheme to encrypt passwords in config files 6: # TODO: Think of a way of validating values - e.g. a value must be an integer, or match a regex 7: 8: use strict; 9: use warnings; 10: 11: use Carp; 12: use JSON::MaybeXS 'decode_json'; # Doesn't behave well with require 13: use File::Slurp qw(read_file); 14: use File::Spec; 15: use Hash::Merge qw(merge); 16: use Params::Get 0.14; 17: use Params::Validate::Strict 0.11; 18: use Scalar::Util; 19: 20: =head1 NAME 21: 22: Config::Abstraction - Merge and manage configuration data from different sources 23: 24: =head1 VERSION 25: 26: Version 0.39 27: 28: =cut 29: 30: our $VERSION = '0.39'; 31: 32: =head1 SYNOPSIS 33: 34: C<Config::Abstraction> lets you load configuration from multiple sources, 35: such as files, environment variables, and in-code defaults, 36: and merge them with predictable precedence. 37: It provides a consistent API for accessing the configuration settings, regardless of where they came from, 38: this helps keep your application's or class's configuration flexible, centralized, and easy to override. 39: 40: use Config::Abstraction; 41: 42: my $config = Config::Abstraction->new( 43: config_dirs => ['config'], 44: env_prefix => 'APP_', 45: flatten => 0, 46: ); 47: 48: my $db_user = $config->get('database.user'); 49: 50: =head1 DESCRIPTION 51: 52: C<Config::Abstraction> is a flexible configuration management layer that sits above C<Config::*> modules. 53: It provides a simple way to layer multiple configuration sources with predictable merge order. 54: It lets you define sources such as: 55: 56: =over 4 57: 58: =item * Perl hashes (in-memory defaults or dynamic values) 59: 60: =item * Environment variables (with optional prefixes) 61: 62: =item * Configuration files (YAML, JSON, INI, or plain key=value) 63: 64: =item * Command-line arguments 65: 66: =back 67: 68: Sources are applied in the order they are provided. Later sources override 69: earlier ones unless a key is explicitly set to C<undef> in the later source. 70: 71: In addition to using drivers to load configuration data from multiple file 72: formats (YAML, JSON, XML, and INI), 73: it also allows levels of configuration, each of which overrides the lower levels. 74: So, it also integrates environment variable 75: overrides and command line arguments for runtime configuration adjustments. 76: This module is designed to help developers manage layered configurations that can be loaded from files and overridden at run-time for debugging, 77: offering a modern, robust and dynamic approach 78: to configuration management. 79: 80: =head2 Merge Precedence Diagram 81: 82: +----------------+ 83: | CLI args | (Highest priority) 84: +----------------+ 85: | Environment | 86: +----------------+ 87: | Config file(s) | 88: +----------------+ 89: | Defaults | (Lowest priority) 90: +----------------+ 91: 92: =head2 KEY FEATURES 93: 94: =over 4 95: 96: =item * Multi-Format Support 97: 98: Supports configuration files in YAML, JSON, XML, and INI formats. 99: Automatically merges configuration data from these different formats, 100: allowing hierarchical configuration management. 101: 102: =item * Environment Variable Overrides 103: 104: Allows environment variables to override values in the configuration files. 105: By setting environment variables with a specific prefix (default: C<APP_>), 106: values in the configuration files can be dynamically adjusted without modifying 107: the file contents. 108: 109: =item * Flattened Configuration Option 110: 111: Optionally supports flattening the configuration structure. This converts deeply 112: nested configuration keys into a flat key-value format (e.g., C<database.user> 113: instead of C<database-E<gt>{user}>). This makes accessing values easier for 114: applications that prefer flat structures or need compatibility with flat 115: key-value stores. 116: 117: =item * Layered Configuration 118: 119: Supports merging multiple layers of configuration files. For example, you can 120: have a C<base.yaml> configuration file that provides default values, and a 121: C<local.yaml> (or C<local.json>, C<local.xml>, etc.) file that overrides 122: specific values. This allows for environment-specific configurations while 123: keeping defaults intact. 124: 125: =item * Merge Strategy 126: 127: The module merges the configuration data intelligently, allowing values in more 128: specific files (like C<local.yaml>, C<local.json>, C<local.xml>, C<local.ini>) 129: to override values in base files. This enables a flexible and layered configuration 130: system where you can set defaults and override them for specific environments. 131: 132: =item * Error Handling 133: 134: Includes error handling for loading configuration files. 135: If any file fails to 136: load (e.g., due to syntax issues), the module will throw descriptive error 137: messages to help with debugging. 138: 139: =back 140: 141: =head2 SUPPORTED FILE FORMATS 142: 143: =over 4 144: 145: =item * YAML (C<*.yaml>, C<*.yml>) 146: 147: The module supports loading YAML files using the C<YAML::XS> module. 148: 149: =item * JSON (C<*.json>) 150: 151: The module supports loading JSON files using C<JSON::MaybeXS>. 152: 153: =item * XML (C<*.xml>) 154: 155: The module supports loading XML files using C<XML::Simple>. 156: 157: =item * INI (C<*.ini>) 158: 159: The module supports loading INI files using C<Config::IniFiles>. 160: 161: =back 162: 163: =head2 ENVIRONMENT VARIABLE HANDLING 164: 165: Configuration values can be overridden via environment variables. Environment variables use double underscores (__) to denote nested configuration keys and single underscores remain as part of the key name under the prefix namespace. 166: 167: For example: 168: 169: APP_DATABASE__USER becomes database.user (nested structure) 170: 171: $ export APP_DATABASE__USER="env_user" 172: 173: will override any value set for `database.user` in the configuration files. 174: 175: APP_LOGLEVEL becomes APP.loglevel (flat under prefix namespace) 176: 177: APP_API__RATE_LIMIT becomes api.rate_limit (mixed usage) 178: 179: This allows you to override both top-level and nested configuration values using environment variables. 180: 181: Configuration values can be overridden via the command line (C<@ARGV>). 182: For instance, if you have a key in the configuration such as C<database.user>, 183: you can override it by adding C<"--APP_DATABASE__USER=other_user_name"> to the command line arguments. 184: This will override any value set for C<database.user> in the configuration files. 185: 186: =head2 EXAMPLE CONFIGURATION FLOW 187: 188: =over 4 189: 190: =item 1. Data Argument 191: 192: The data passed into the constructor via the C<data> argument is the starting point. 193: Essentially, 194: this contains the default values. 195: 196: =item 2. Loading Files 197: 198: The module then looks for configuration files in the specified directories. 199: It loads the following files in order of preference: 200: C<base.yaml>, C<local.yaml>, C<base.json>, C<local.json>, C<base.xml>, 201: C<local.xml>, C<base.ini>, and C<local.ini>. 202: 203: If C<config_file> or C<config_files> is set, those files are loaded last. 204: 205: If no C<config_dirs> is given, try hard to find the files in various places. 206: 207: =item 3. Merging and Resolving 208: 209: The module merges the contents of these files, with more specific configurations 210: (e.g., C<local.*>) overriding general ones (e.g., C<base.*>). 211: 212: =item 4. Environment Overrides 213: 214: After loading and merging the configuration files, 215: the environment variables are 216: checked and used to override any conflicting settings. 217: 218: =item 5. Command Line 219: 220: Next, the command line arguments are checked and used to override any conflicting settings. 221: 222: =item 6. Accessing Values 223: 224: Values in the configuration can be accessed using a dotted notation 225: (e.g., C<'database.user'>), regardless of the file format used. 226: 227: =back 228: 229: =head1 METHODS 230: 231: =head2 new 232: 233: Constructor for creating a new configuration object. 234: 235: Options: 236: 237: =over 4 238: 239: =item * C<config_dirs> 240: 241: An arrayref of directories to look for configuration files 242: (default: C<$CONFIG_DIR>, C<$HOME/.conf>, C<$HOME/config>, C<$HOME/conf>, C<$DOCUMENT_ROOT/conf>, C<$DOCUMENT_ROOT/../conf>, C<conf>). 243: 244: =item * C<config_file> 245: 246: Points to a configuration file of any format. 247: 248: =item * C<config_files> 249: 250: An arrayref of files to look for in the configuration directories. 251: Put the more important files later, 252: since later files override earlier ones. 253: 254: Considers the files C<default> and C<$script_name> before looking at C<config_file> and C<config_files>. 255: 256: =item * C<data> 257: 258: A hash ref of default data to prime the configuration with. 259: These are applied before loading 260: other sources and can be overridden by later sources or by explicitly passing 261: options directly to C<new>. 262: 263: $config = Config::Abstraction->new( 264: data => { 265: log_level => 'info', 266: retries => 3, 267: } 268: ); 269: 270: =item * C<defaults> 271: 272: A hash reference that provides default values for the object's own attributes (such as C<config_dirs>, C<logger>, C<flatten>, etc.). 273: If this option is supplied, 274: the object is initialized using the keys in this hash as the base; 275: any other options passed directly to C<new()> (aside from C<env_prefix>) are ignored. 276: This allows you to pre-define a standard configuration profile for the object itself. 277: Note that C<defaults> is distinct from the C<data> option - C<data> supplies the initial configuration values that will be merged with files, environment, and command line, 278: while C<defaults> sets the object's internal parameters. 279: The C<env_prefix> value, 280: if provided as a top-level argument, 281: still takes precedence over any C<env_prefix> that might exist inside the C<defaults> hash. 282: 283: =item * C<env_prefix> 284: 285: A prefix for environment variable keys and comment line options, e.g. C<MYAPP_DATABASE__USER>, 286: (default: C<'APP_'>). 287: 288: =item * C<file> 289: 290: Synonym for C<config_file> 291: 292: =item * C<flatten> 293: 294: If true, returns a flat hash structure like C<{database.user}> (default: C<0>) instead of C<{database}{user}>. 295: ` 296: =item * C<level> 297: 298: Level for logging. 299: 300: =item * C<logger> 301: 302: Used for warnings and traces. 303: It can be an object that understands warn() and trace() messages, 304: such as a L<Log::Log4perl> or L<Log::Any> object, 305: a reference to code, 306: a reference to an array, 307: or a filename. 308: 309: =item * C<path> 310: 311: A synonym of C<config_dirs>. 312: 313: =item * C<sep_char> 314: 315: The separator in keys. 316: The default is a C<'.'>, 317: as in dotted notation, 318: such as C<'database.user'>. 319: 320: =item * C<schema> 321: 322: A L<Params::Validate::Strict> compatible schema to validate the configuration file against. 323: 324: =back 325: 326: If just one argument is given, it is assumed to be the name of a file. 327: 328: =cut 329: 330: sub new 331: { โ332 โ 335 โ 342โ332 โ 335 โ 0 332: my $class = shift; 333: my $params; 334: 335: if(scalar(@_) == 1) {Mutants (Total: 2, Killed: 2, Survived: 0)
336: # Just one parameter - the name of a file 337: $params = Params::Get::get_params('file', \@_); 338: } else { 339: $params = Params::Get::get_params(undef, \@_) || {}; 340: } 341: โ342 โ 346 โ 375โ342 โ 346 โ 0 342: $params->{'config_dirs'} //= $params->{'path'}; # Compatibility with Config::Auto 343: 344: $params->{'config_file'} //= $params->{'file'} if($params->{'file'}); 345: 346: if(!defined($params->{'config_dirs'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
347: if($params->{'config_file'} && File::Spec->file_name_is_absolute($params->{'config_file'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
348: $params->{'config_dirs'} = ['']; 349: } else { 350: # Set up the default value for config_dirs 351: if($^O ne 'MSWin32') {
352: $params->{'config_dirs'} = [ '/etc', '/usr/local/etc' ]; 353: } else { 354: $params->{'config_dirs'} = ['']; 355: } 356: if($ENV{'HOME'}) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_351_4: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
357: push @{$params->{'config_dirs'}}, 358: File::Spec->catdir($ENV{'HOME'}, '.conf'), 359: File::Spec->catdir($ENV{'HOME'}, '.config'), 360: File::Spec->catdir($ENV{'HOME'}, 'conf'), 361: } elsif($ENV{'DOCUMENT_ROOT'}) { 362: push @{$params->{'config_dirs'}}, 363: File::Spec->catdir($ENV{'DOCUMENT_ROOT'}, File::Spec->updir(), 'conf'), 364: File::Spec->catdir($ENV{'DOCUMENT_ROOT'}, 'conf'), 365: File::Spec->catdir($ENV{'DOCUMENT_ROOT'}, 'config'); 366: } 367: if(my $dir = $ENV{'CONFIG_DIR'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
368: push @{$params->{'config_dirs'}}, $dir; 369: } else { 370: push @{$params->{'config_dirs'}}, 'conf', 'config'; 371: } 372: } 373: } 374: โ375 โ 382 โ 397โ375 โ 382 โ 0 375: my $self = bless { 376: sep_char => '.', 377: %{$params->{defaults} ? $params->{defaults} : $params}, 378: env_prefix => $params->{env_prefix} || 'APP_', 379: config => {}, 380: }, $class; 381: 382: if(my $logger = $self->{'logger'}) {
383: if(!Scalar::Util::blessed($logger)) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_382_2: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes384: # Don't call $self->_load_driver('Log::Abstraction') as it can make a call to logger, which is yet to be set up 385: eval "require Log::Abstraction"; 386: if($@) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_383_3: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes387: carp(ref($self), ": Log::Abstraction failed to load: $@"); 388: } else { 389: Log::Abstraction->import(); 390: $self->{'logger'} = Log::Abstraction->new($logger); 391: if($params->{'level'} && $self->{'logger'}->can('level')) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_386_4: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes392: $self->{'logger'}->level($params->{'level'}); 393: } 394: } 395: } 396: } โ397 โ 399 โ 403โ397 โ 399 โ 0 397: $self->_load_config(); 398: 399: if(my $schema = $params->{'schema'}) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_391_5: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
400: $self->{'config'} = Params::Validate::Strict::validate_strict(schema => $schema, input => $self->{'config'}); 401: } 402: โ403 โ 403 โ 406โ403 โ 403 โ 0 403: if(defined($self->{'config'}) && scalar(keys %{$self->{'config'}})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
404: return $self;
Mutants (Total: 2, Killed: 2, Survived: 0)
405: } โ406 โ 406 โ 0 406: return undef;
Mutants (Total: 2, Killed: 2, Survived: 0)
407: } 408: 409: # Determine if a value is a plain, unblessed, non-reference scalar 410: # safe to use in regex/string operations. 411: # Args: value to test 412: # Returns: 1 if plain scalar, 0 otherwise 413: sub _is_plain_scalar 414: { 415: my $val = $_[0]; 416: 417: return 0 if !defined($val);
Mutants (Total: 2, Killed: 2, Survived: 0)
418: return 0 if Scalar::Util::blessed($val);
Mutants (Total: 2, Killed: 2, Survived: 0)
419: return 0 if ref($val);
Mutants (Total: 2, Killed: 2, Survived: 0)
420: return 1;
Mutants (Total: 2, Killed: 2, Survived: 0)
421: } 422: 423: sub _load_config 424: { โ425 โ 425 โ 429โ425 โ 425 โ 0 425: if(!UNIVERSAL::isa((caller)[0], __PACKAGE__)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
426: Carp::croak('Illegal Operation: This method can only be called by a subclass'); 427: } 428: โ429 โ 432 โ 437โ429 โ 432 โ 0 429: my $self = shift; 430: my %merged; 431: 432: if($self->{'data'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
433: # The data argument given to 'new' contains defaults that this routine will override 434: %merged = %{$self->{'data'}}; 435: } 436: โ437 โ 438 โ 442โ437 โ 438 โ 0 437: my $logger = $self->{'logger'}; 438: if($logger) {
Mutants (Total: 1, Killed: 1, Survived: 0)
439: $logger->trace(ref($self), ' ', __LINE__, ': Entered _load_config'); 440: } 441: โ442 โ 443 โ 452โ442 โ 443 โ 0 442: my @dirs = @{$self->{'config_dirs'}}; 443: if($self->{'config_file'} && (scalar(@dirs) > 1)) {
Mutants (Total: 4, Killed: 4, Survived: 0)
444: if(File::Spec->file_name_is_absolute($self->{'config_file'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
445: # Handle absolute paths 446: @dirs = (''); 447: } else { 448: # Look in the current directory 449: push @dirs, File::Spec->curdir(); 450: } 451: } โ452 โ 452 โ 719โ452 โ 452 โ 0 452: for my $dir (@dirs) { 453: next if(!defined($dir)); 454: if(length($dir) && !-d $dir) {
Mutants (Total: 1, Killed: 1, Survived: 0)
455: next; 456: } 457: 458: for my $file (qw/base.yaml base.yml base.json base.xml base.ini local.yaml local.yml local.json local.xml local.ini/) { 459: my $path = File::Spec->catfile($dir, $file); 460: if($logger) {
Mutants (Total: 1, Killed: 1, Survived: 0)
461: $logger->debug(ref($self), ' ', __LINE__, ": Looking for configuration $path"); 462: } 463: next unless -f $path; 464: next unless -r $path; 465: 466: if($logger) {
Mutants (Total: 1, Killed: 1, Survived: 0)
467: $logger->debug(ref($self), ' ', __LINE__, ": Loading data from $path"); 468: } 469: 470: my $data; 471: # Only load config modules when they are needed 472: if ($file =~ /\.ya?ml$/) {
Mutants (Total: 1, Killed: 1, Survived: 0)
473: $self->_load_driver('YAML::XS', ['LoadFile']); 474: $data = eval { LoadFile($path) }; 475: if($@) {
Mutants (Total: 1, Killed: 1, Survived: 0)
476: if($logger) {
477: $logger->notice("Failed to load YAML from $path: $@"); 478: } else { 479: Carp::carp("Failed to load YAML from $path: $@"); 480: } 481: next; 482: } 483: } elsif ($file =~ /\.json$/) { 484: $data = eval { decode_json(read_file($path)) }; 485: if($@) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_476_6: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
486: if($logger) {
Mutants (Total: 1, Killed: 1, Survived: 0)
487: $logger->notice("Failed to load JSON from $path: $@"); 488: } else { 489: Carp::carp("Failed to load JSON from $path: $@"); 490: } 491: next; 492: } 493: } elsif($file =~ /\.xml$/) { 494: my $rc; 495: if($self->_load_driver('XML::Simple', ['XMLin'])) {
496: eval { $rc = XMLin($path, ForceArray => 0, KeyAttr => []) }; 497: if($@) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_495_5: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes498: if($logger) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_497_6: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes499: $logger->notice("Failed to load XML from $path: $@"); 500: } else { 501: Carp::carp("Failed to load XML from $path: $@"); 502: } 503: undef $rc; 504: } elsif($rc) { 505: $data = $rc; 506: } 507: } 508: if((!defined($rc)) && $self->_load_driver('XML::PP')) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_498_7: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
509: my $xml_pp = XML::PP->new(); 510: $data = read_file($path); 511: if(my $tree = $xml_pp->parse(\$data)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
512: if($data = $xml_pp->collapse_structure($tree)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
513: $self->{'type'} = 'XML'; 514: if($data->{'config'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
515: $data = $data->{'config'}; 516: } 517: } 518: } 519: } 520: } elsif ($file =~ /\.ini$/) { 521: $self->_load_driver('Config::IniFiles'); 522: if(my $ini = Config::IniFiles->new(-file => $path)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
523: $data = { map { 524: my $section = $_; 525: $section => { map { $_ => $ini->val($section, $_) } $ini->Parameters($section) } 526: } $ini->Sections() }; 527: } else { 528: if($logger) {
529: $logger->notice("Failed to load INI from $path: $@"); 530: } else { 531: Carp::carp("Failed to load INI from $path: $@"); 532: } 533: } 534: } 535: if($data) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_528_6: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
536: if(!ref($data)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
537: if($logger) {
538: $logger->debug(ref($self), ' ', __LINE__, ": ignoring data from $path ($data)"); 539: } 540: next; 541: } 542: if(ref($data) ne 'HASH') {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_537_6: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
543: if($logger) {
Mutants (Total: 1, Killed: 1, Survived: 0)
544: $logger->debug(ref($self), ' ', __LINE__, ": ignoring data from $path (not a hashref)"); 545: } 546: next; 547: } 548: if($logger) {
Mutants (Total: 1, Killed: 1, Survived: 0)
549: $logger->debug(ref($self), ' ', __LINE__, ": Loaded data from $path"); 550: } 551: %merged = %{ merge( $data, \%merged ) }; 552: push @{$merged{'config_path'}}, $path; 553: } 554: } 555: 556: # Put $self->{config_file} through all parsers, ignoring all errors, then merge that in 557: if(!$self->{'script_name'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
558: require File::Basename && File::Basename->import() unless File::Basename->can('basename'); 559: 560: # Determine script name 561: $self->{'script_name'} = File::Basename::basename($ENV{'SCRIPT_NAME'} || $0); 562: } 563: 564: my $script_name = $self->{'script_name'}; 565: for my $config_file ('default', $script_name, "$script_name.cfg", "$script_name.conf", "$script_name.config", $self->{'config_file'}, @{$self->{'config_files'}}) { 566: next unless defined($config_file); 567: # Note that loading $script_name in the current directory could mean loading the script as it's own config. 568: # This test is not foolproof, buyer beware 569: next if(($config_file eq $script_name) && ((length($dir) == 0) || ($dir eq File::Spec->curdir())));
570: my $path = length($dir) ? File::Spec->catfile($dir, $config_file) : $config_file; 571: if($logger) {Mutants (Total: 1, Killed: 0, Survived: 1)
- NUM_BOUNDARY_569_61_!=: Numeric boundary flip == to !=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );Mutants (Total: 1, Killed: 1, Survived: 0)
572: $logger->debug(ref($self), ' ', __LINE__, ": Looking for configuration $path"); 573: } 574: if((-f $path) && (-r $path)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
575: my $data = read_file($path); 576: if($logger) {
Mutants (Total: 1, Killed: 1, Survived: 0)
577: $logger->debug(ref($self), ' ', __LINE__, ": Loading data from $path"); 578: } 579: eval { 580: if(($data =~ /^\s*<\?xml/) || ($data =~ /<\/.+>/)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
581: if($self->_load_driver('XML::Simple', ['XMLin'])) {
Mutants (Total: 1, Killed: 1, Survived: 0)
582: if($data = XMLin($path, ForceArray => 0, KeyAttr => [])) {
583: $self->{'type'} = 'XML'; 584: } 585: } elsif($self->_load_driver('XML::PP')) { 586: my $xml_pp = XML::PP->new(); 587: if(my $tree = $xml_pp->parse(\$data)) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_582_8: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
588: if($data = $xml_pp->collapse_structure($tree)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
589: $self->{'type'} = 'XML'; 590: if($data->{'config'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
591: $data = $data->{'config'}; 592: } 593: } 594: } 595: } 596: } elsif($data =~ /\{.+:.\}/s) { 597: $self->_load_driver('JSON::Parse'); 598: # CPanel::JSON is very noisy, so be careful before attempting to use it 599: my $is_json; 600: eval { $is_json = JSON::Parse::parse_json($data) }; 601: if($is_json) {
602: eval { $data = decode_json($data) }; 603: if($@) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_601_7: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes604: undef $data; 605: } 606: } else { 607: undef $data; 608: } 609: if($data) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_603_8: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes610: $self->{'type'} = 'JSON'; 611: } 612: } else { 613: undef $data; 614: } 615: if(!$data) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_609_7: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
616: $self->_load_driver('YAML::XS', ['LoadFile']); 617: if((eval { $data = LoadFile($path) }) && (ref($data) eq 'HASH')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
618: # Could be colon file, could be YAML, whichever it is break the configuration fields 619: # foreach my($k, $v) (%{$data}) { 620: foreach my $k (keys %{$data}) { 621: my $v = $data->{$k}; 622: if(!defined($v)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
623: # e.g. a simple line 624: # foo: 625: # with nothing under it 626: $data->{$k} = undef; 627: next; 628: } 629: # Do not inspect or modify coderefs, blessed objects, or any reference 630: next unless _is_plain_scalar($v); 631: 632: next if($v =~ /^".+"$/); # Quotes to keep in one field 633: if($v =~ /,/) {
Mutants (Total: 1, Killed: 1, Survived: 0)
634: my @vals = split(/\s*,\s*/, $v); 635: delete $data->{$k}; 636: foreach my $val (@vals) { 637: if($val =~ /(.+)=(.+)/) {
Mutants (Total: 1, Killed: 1, Survived: 0)
638: $data->{$k}{$1} = $2; 639: } else { 640: $data->{$k}{$val} = 1; 641: } 642: } 643: } 644: } 645: if($data) {
646: $self->{'type'} = 'YAML'; 647: } 648: } 649: if((!$data) || (ref($data) ne 'HASH')) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_645_8: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
650: $self->_load_driver('Config::IniFiles'); 651: if(my $ini = Config::IniFiles->new(-file => $path)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
652: $data = { map { 653: my $section = $_; 654: $section => { map { $_ => $ini->val($section, $_) } $ini->Parameters($section) } 655: } $ini->Sections() }; 656: if($data) {
657: $self->{'type'} = 'INI'; 658: } 659: } 660: if((!$data) || (ref($data) ne 'HASH')) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_656_9: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
661: # Maybe XML without the leading XML header 662: if($self->_load_driver('XML::Simple', ['XMLin'])) {
663: eval { $data = XMLin($path, ForceArray => 0, KeyAttr => []) }; 664: } 665: if((!$data) || (ref($data) ne 'HASH')) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_662_9: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes666: if($self->_load_driver('Config::Abstract')) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_665_9: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes667: # Handle RT#164587 668: open my $oldSTDERR, '>&STDERR'; 669: close STDERR; 670: eval { $data = Config::Abstract->new($path) }; 671: my $err = $@; 672: open STDERR, '>&', $oldSTDERR; 673: if($err) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_666_10: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes674: undef $data; 675: } elsif($data) { 676: $data = $data->get_all_settings(); 677: if(scalar(keys %{$data}) == 0) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_673_11: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes678: undef $data; 679: } 680: } 681: $self->{'type'} = 'Perl'; 682: } 683: } 684: if((!$data) || (ref($data) ne 'HASH')) {Mutants (Total: 2, Killed: 0, Survived: 2)
- NUM_BOUNDARY_677_37_!=: Numeric boundary flip == to !=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- COND_INV_677_12: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
685: $self->_load_driver('Config::Auto'); 686: my $ca = Config::Auto->new(source => $path); 687: if($data = $ca->parse()) {
688: $self->{'type'} = $ca->format(); 689: } 690: } 691: } 692: } 693: } 694: }; 695: if($logger) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_687_10: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
696: if($@) {
697: $logger->warn(ref($self), ' ', __LINE__, ": $@"); 698: undef $data; 699: } else { 700: $logger->debug(ref($self), ' ', __LINE__, ': Loaded data from', $self->{'type'}, "file $path"); 701: } 702: } 703: if(scalar(keys %merged)) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_696_6: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
704: if($data) {
Mutants (Total: 1, Killed: 1, Survived: 0)
705: %merged = %{ merge($data, \%merged) }; 706: } 707: } elsif($data && (ref($data) eq 'HASH')) { 708: %merged = %{$data}; 709: } elsif((!$@) && $logger) { 710: $logger->debug(ref($self), ' ', __LINE__, ': No configuration file loaded'); 711: } 712: 713: push @{$merged{'config_path'}}, $path; 714: } 715: } 716: } 717: 718: # Merge ENV vars โ719 โ 723 โ 737โ719 โ 723 โ 0 719: my $prefix = $self->{env_prefix}; 720: $prefix =~ s/__$//; 721: $prefix =~ s/_$//; 722: $prefix =~ s/::$//; 723: for my $key (keys %ENV) { 724: next unless $key =~ /^$self->{env_prefix}(.*)$/i; 725: my $path = lc($1); 726: if($path =~ /__/) {
Mutants (Total: 1, Killed: 1, Survived: 0)
727: my @parts = split /__/, $path; 728: my $ref = \%merged; 729: $ref = ($ref->{$_} //= {}) for @parts[0..$#parts-1]; 730: $ref->{ $parts[-1] } = $ENV{$key}; 731: } else { 732: $merged{$prefix}->{$path} = $ENV{$key}; 733: } 734: } 735: 736: # Merge command line options โ737 โ 737 โ 753โ737 โ 737 โ 0 737: foreach my $arg(@ARGV) { 738: next unless($arg =~ /=/); 739: my ($key, $value) = split(/=/, $arg, 2); 740: next unless $key =~ /^\-\-$self->{env_prefix}(.*)$/; 741: 742: my $path = lc($1); 743: my @parts = split(/__/, $path); 744: if(scalar(@parts) > 0) {
Mutants (Total: 4, Killed: 4, Survived: 0)
745: my $ref = \%merged; 746: if(scalar(@parts) > 1) {
Mutants (Total: 4, Killed: 4, Survived: 0)
747: $ref = ($ref->{$_} //= {}) for @parts[0..$#parts-1]; 748: } 749: $ref->{$parts[-1]} = $value; 750: } 751: } 752: โ753 โ 753 โ 760โ753 โ 753 โ 0 753: if($self->{'flatten'}) {
754: $self->_load_driver('Hash::Flatten', ['flatten']); 755: } else { 756: $self->_load_driver('Hash::Flatten', ['unflatten']); 757: } 758: # $self->{config} = $self->{flatten} ? flatten(\%merged) : unflatten(\%merged); 759: # Don't unflatten because of RT#166761 โ760 โ 760 โ 0 760: $self->{config} = $self->{flatten} ? flatten(\%merged) : \%merged; 761: } 762: 763: =head2 get(key) 764: 765: Retrieve a configuration value using dotted key notation (e.g., 766: C<'database.user'>). Returns C<undef> if the key doesn't exist. 767: 768: =cut 769: 770: sub get 771: { โ772 โ 774 โ 777โ772 โ 774 โ 0 772: my ($self, $key) = @_; 773: 774: if($self->{flatten}) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_753_2: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
775: return $self->{config}{$key};
Mutants (Total: 2, Killed: 2, Survived: 0)
776: } โ777 โ 778 โ 783โ777 โ 778 โ 0 777: my $ref = $self->{'config'}; 778: for my $part (split qr/\Q$self->{sep_char}\E/, $key) { 779: return undef unless ref $ref eq 'HASH';
Mutants (Total: 2, Killed: 2, Survived: 0)
780: return unless exists $ref->{$part}; 781: $ref = $ref->{$part}; 782: } โ783 โ 783 โ 802โ783 โ 783 โ 0 783: if((defined($ref) && (ref($ref) eq 'HASH') && !$self->{'no_fixate'})) {
784: if($self->_load_data_reuse()) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_783_2: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes785: if(ref($ref) eq 'HASH') {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_784_3: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes786: if(!tied %$ref) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_785_4: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes787: # Pass the hashref directly (not dereferenced) so fixate receives 788: # a named scalar it can make read-only without flattening the hash 789: # FIXME: 790: # What works on MacOS doesn't work 791: # on Linux and vice versa. 792: # Something is wrong. 793: # Data::Reuse::fixate(%{$ref}) if scalar(keys %{$ref}); 794: # Data::Reuse::fixate($ref) if scalar(keys %{$ref}); 795: } 796: } elsif(ref($ref) eq 'ARRAY') { 797: # RT#171980 798: # Data::Reuse::fixate(@{$ref}); 799: } 800: } 801: } โ802 โ 802 โ 0 802: return $ref;Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_786_5: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 2, Killed: 2, Survived: 0)
803: } 804: 805: sub _load_data_reuse 806: { โ807 โ 820 โ 825โ807 โ 820 โ 0 807: my $self = $_[0]; 808: 809: # Skip fixation entirely if caller has opted out 810: return 0 if($self->{'no_fixate'});
Mutants (Total: 2, Killed: 2, Survived: 0)
811: 812: # Return cached result to avoid repeated require attempts 813: return 1 if($self->{reuse_loaded});
814: return 0 if($self->{reuse_failed});Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_813_2: Negate boolean return expression
MEDIUM: Add tests asserting both true and false outcomes๐งช Suggested Test# Boolean branch test suggestion ok( !func(INPUT), 'Verify boolean branch behaviour' );- RETURN_UNDEF_813_2: Replace return expression with undef
LOW: Mutation survived, but impact may be minor๐งช Suggested Test# Return value assertion is( func(INPUT), EXPECTED, 'Verify correct return value' );Mutants (Total: 2, Killed: 2, Survived: 0)
815: 816: eval { 817: require Data::Reuse; 818: Data::Reuse->import(); 819: }; 820: if($@) {
821: # Cache the failure so we do not attempt to load again 822: $self->{reuse_failed} = 1; 823: return 0;Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_820_2: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 2, Killed: 2, Survived: 0)
824: } โ[NOT COVERED] 825 โ 826 โ 0 825: $self->{reuse_loaded} = 1; 826: return 1;
827: } 828: 829: =head2 exists(key) 830: 831: Does a configuration value using dotted key notation (e.g., C<'database.user'>) exist? 832: Returns 0 or 1. 833: 834: =cut 835: 836: sub exists 837: { โ838 โ 840 โ 843โ838 โ 840 โ 0 838: my ($self, $key) = @_; 839: 840: if($self->{flatten}) {Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_826_2: Negate boolean return expression
MEDIUM: Add tests asserting both true and false outcomes๐งช Suggested Test# Boolean branch test suggestion ok( !func(INPUT), 'Verify boolean branch behaviour' );- RETURN_UNDEF_826_2: Replace return expression with undef
LOW: Mutation survived, but impact may be minor๐งช Suggested Test# Return value assertion is( func(INPUT), EXPECTED, 'Verify correct return value' );Mutants (Total: 1, Killed: 1, Survived: 0)
841: return exists($self->{config}{$key}) ? 1 : 0; 842: } โ843 โ 844 โ 849โ843 โ 844 โ 0 843: my $ref = $self->{'config'}; 844: for my $part (split qr/\Q$self->{sep_char}\E/, $key) { 845: return 0 unless ref $ref eq 'HASH';
Mutants (Total: 2, Killed: 2, Survived: 0)
846: return 0 if(!exists($ref->{$part}));
Mutants (Total: 2, Killed: 2, Survived: 0)
847: $ref = $ref->{$part}; 848: } โ849 โ 849 โ 0 849: return 1;
Mutants (Total: 2, Killed: 2, Survived: 0)
850: } 851: 852: =head2 all() 853: 854: Returns the entire configuration hash, 855: possibly flattened depending on the C<flatten> option. 856: 857: The entry C<config_path> contains a list of the files that the configuration was loaded from. 858: 859: =cut 860: 861: sub all 862: { 863: my $self = shift; 864: 865: return if(!$self->{config}); 866: 867: # This is good for debugging, but not much more and it breaks inheritance, so disabled 868: # if($self->_load_data_reuse()) { 869: # Data::Reuse::fixate($self->{config}); 870: # } 871: 872: return(scalar(keys %{$self->{'config'}})) ? $self->{'config'} : undef; 873: } 874: 875: =head2 merge_defaults 876: 877: Merge the configuration hash into the given hash. 878: 879: package MyPackage; 880: use Params::Get; 881: use Config::Abstraction; 882: 883: sub new 884: { 885: my $class = shift; 886: 887: my $params = Params::Get::get_params(undef, \@_) || {}; 888: 889: if(my $config = Config::Abstraction->new(env_prefix => "${class}::")) { 890: $params = $config->merge_defaults(defaults => $params, merge => 1, section => $class); 891: } 892: 893: return bless $params, $class; 894: } 895: 896: Options: 897: 898: =over 4 899: 900: =item * merge 901: 902: Usually, 903: what's in the object will overwrite what's in the defaults hash, 904: if given, 905: the result will be a combination of the hashes. 906: 907: =item * section 908: 909: Merge in that section from the configuration file. 910: 911: =item * deep 912: 913: Try harder to merge all configurations from the global section of the configuration file. 914: 915: =back 916: 917: =cut 918: 919: sub merge_defaults 920: { โ921 โ 933 โ 941โ921 โ 933 โ 0 921: my $self = shift; 922: my $config = $self->all(); 923: 924: return $config if(scalar(@_) == 0);
Mutants (Total: 3, Killed: 3, Survived: 0)
925: 926: my $params = Params::Get::get_params('defaults', @_); 927: my $defaults = $params->{'defaults'}; 928: return $config if(!defined($defaults));
Mutants (Total: 2, Killed: 2, Survived: 0)
929: my $section = $params->{'section'}; 930: 931: Hash::Merge::set_clone_behavior(0); 932: 933: if(exists $config->{'global'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
934: if($params->{'deep'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
935: $defaults = merge($config->{'global'}, $defaults); 936: } else { 937: $defaults = { %{$defaults}, %{$config->{'global'}} }; 938: } 939: delete $config->{'global'}; 940: } โ941 โ 941 โ 944โ941 โ 941 โ 0 941: if($section && exists $config->{$section}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
942: $config = $config->{$section}; 943: } โ944 โ 944 โ 947โ944 โ 944 โ 0 944: if($params->{'merge'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
945: return merge($config, $defaults); 946: } โ947 โ 947 โ 0 947: return { %{$defaults}, %{$config} }; 948: } 949: 950: # Helper routine to load a driver. 951: # NOTE: Log::Abstraction must NOT be loaded via this method - it is 952: # bootstrapped directly in new() to avoid a circular initialisation 953: # dependency where _load_driver would attempt to log via an as-yet 954: # uninitialised logger. 955: sub _load_driver 956: { โ957 โ 963 โ 970โ957 โ 963 โ 0 957: my($self, $driver, $imports) = @_; 958: 959: return 1 if($self->{'loaded'}{$driver});
Mutants (Total: 2, Killed: 2, Survived: 0)
960: return 0 if($self->{'failed'}{$driver});
Mutants (Total: 2, Killed: 2, Survived: 0)
961: 962: eval "require $driver"; 963: if($@) {
Mutants (Total: 1, Killed: 1, Survived: 0)
964: if(my $logger = $self->{'logger'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
965: $logger->warn(ref($self), ": $driver failed to load: $@"); 966: } 967: $self->{'failed'}{$driver} = 1; 968: return; 969: } โ970 โ 972 โ 0 970: $driver->import(@{ $imports // [] }); 971: $self->{'loaded'}{$driver} = 1; 972: return 1;
Mutants (Total: 2, Killed: 2, Survived: 0)
973: } 974: 975: =head2 AUTOLOAD 976: 977: This module supports dynamic access to configuration keys via AUTOLOAD. 978: Nested keys are accessible using the separator, 979: so C<$config-E<gt>database_user()> resolves to C<< $config->{database}->{user} >>, 980: when C<sep_char> is set to '_'. 981: 982: $config = Config::Abstraction->new( 983: data => { 984: database => { 985: user => 'alice', 986: pass => 'secret' 987: }, 988: log_level => 'debug' 989: }, 990: flatten => 1, 991: sep_char => '_' 992: ); 993: 994: my $user = $config->database_user(); # returns 'alice' 995: 996: # or 997: $user = $config->database()->{'user'}; # returns 'alice' 998: 999: # Attempting to call a nonexistent key 1000: my $foo = $config->nonexistent_key(); # dies with error 1001: 1002: =cut 1003: 1004: sub AUTOLOAD 1005: { โ1006 โ 1020 โ 1024โ1006 โ 1020 โ 0 1006: our $AUTOLOAD; 1007: 1008: my $self = shift; 1009: my $key = $AUTOLOAD; 1010: 1011: $key =~ s/.*:://; # remove package name 1012: return if $key eq 'DESTROY'; 1013: 1014: # my $val = $self->get($key); 1015: # return $val if(defined($val)); 1016: 1017: my $data = $self->{data} || $self->{'config'}; 1018: 1019: # If flattening is ON, assume keys are pre-flattened 1020: if ($self->{flatten}) {
1021: return $data->{$key} if(exists $data->{$key});Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_1020_2: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes1022: } 1023: โ1024 โ 1028 โ 1035โ1024 โ 1028 โ 0 1024: my $sep = $self->{'sep_char'}; 1025: 1026: # Fallback: try resolving nested structure dynamically 1027: my $val = $data; 1028: foreach my $part(split /\Q$sep\E/, $key) { 1029: if((ref($val) eq 'HASH') && (exists $val->{$part})) {Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_1021_3: Negate boolean return expression
MEDIUM: Add tests asserting both true and false outcomes๐งช Suggested Test# Boolean branch test suggestion ok( !func(INPUT), 'Verify boolean branch behaviour' );- RETURN_UNDEF_1021_3: Replace return expression with undef
LOW: Mutation survived, but impact may be minor๐งช Suggested Test# Return value assertion is( func(INPUT), EXPECTED, 'Verify correct return value' );Mutants (Total: 1, Killed: 1, Survived: 0)
1030: $val = $val->{$part}; 1031: } else { 1032: croak "No such config key '$key'"; 1033: } 1034: } โ1035 โ 1035 โ 0 1035: return $val;
Mutants (Total: 2, Killed: 2, Survived: 0)
1036: } 1037: 1038: 1; 1039: 1040: =head1 COMMON PITFALLS 1041: 1042: =over 4 1043: 1044: =item * Nested hashes 1045: 1046: Merging replaces entire nested hashes unless you enable deep merging. 1047: 1048: =item * Undef values 1049: 1050: Keys explicitly set to C<undef> in a later source override earlier values. 1051: 1052: =item * Environment 1053: 1054: When using environment variables, 1055: remember that double underscores (__) create nested structures, 1056: while single underscores remain as part of the key name under the prefix namespace. 1057: 1058: =back 1059: 1060: =head1 BUGS 1061: 1062: It should be possible to escape the separator character either with backslashes or quotes. 1063: 1064: Due to the case-insensitive nature of environment variables on Windows, 1065: it may be challenging to override values using environment variables on that platform. 1066: 1067: =head1 REPOSITORY 1068: 1069: L<https://github.com/nigelhorne/Config-Abstraction> 1070: 1071: =head1 SUPPORT 1072: 1073: This module is provided as-is without any warranty. 1074: 1075: Please report any bugs or feature requests to C<bug-config-abstraction at rt.cpan.org>, 1076: or through the web interface at 1077: L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Config-Abstraction>. 1078: I will be notified, and then you'll 1079: automatically be notified of progress on your bug as I make changes. 1080: 1081: You can find documentation for this module with the perldoc command. 1082: 1083: perldoc Config::Abstraction 1084: 1085: =head1 SEE ALSO 1086: 1087: =over 4 1088: 1089: =item * L<Config::Any> 1090: 1091: =item * L<Config::Auto> 1092: 1093: =item * L<Data::Reuse> 1094: 1095: Used to C<fixate()> elements when installed, unless C<no-fixate> is given 1096: 1097: =item * L<Hash::Merge> 1098: 1099: =item * L<Log::Abstraction> 1100: 1101: =item * L<Test Dashboard|https://nigelhorne.github.io/Config-Abstraction/coverage/> 1102: 1103: =item * Development version on GitHub L<https://github.com/nigelhorne/Config-Abstraction> 1104: 1105: =back 1106: 1107: =head1 AUTHOR 1108: 1109: Nigel Horne, C<< <njh at nigelhorne.com> >> 1110: 1111: =cut 1112: 1113: __END__