lib/Config/Abstraction.pm

Structural Coverage (Approximate)

TER1 (Statement): 83.94%
TER2 (Branch): 71.01%
TER3 (LCSAJ): 98.3% (57/58)
Approximate LCSAJ segments: 277

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 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') {

Mutants (Total: 1, Killed: 0, Survived: 1)
352: $params->{'config_dirs'} = [ '/etc', '/usr/local/etc' ]; 353: } else { 354: $params->{'config_dirs'} = ['']; 355: } 356: if($ENV{'HOME'}) {

Mutants (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'}) {

Mutants (Total: 1, Killed: 0, Survived: 1)
383: if(!Scalar::Util::blessed($logger)) {
Mutants (Total: 1, Killed: 0, Survived: 1)
384: # 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)
387: 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)
392: $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: 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) {

Mutants (Total: 1, Killed: 0, Survived: 1)
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: 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'])) {

Mutants (Total: 1, Killed: 0, Survived: 1)
496: eval { $rc = XMLin($path, ForceArray => 0, KeyAttr => []) }; 497: if($@) {
Mutants (Total: 1, Killed: 0, Survived: 1)
498: if($logger) {
Mutants (Total: 1, Killed: 0, Survived: 1)
499: $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: 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) {

Mutants (Total: 1, Killed: 0, Survived: 1)
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: 1, Survived: 0)

536: if(!ref($data)) {

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

537: if($logger) {

Mutants (Total: 1, Killed: 0, Survived: 1)
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: 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())));

Mutants (Total: 1, Killed: 0, Survived: 1)
570: my $path = length($dir) ? File::Spec->catfile($dir, $config_file) : $config_file; 571: if($logger) {

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 => [])) {

Mutants (Total: 1, Killed: 0, Survived: 1)
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: 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) {

Mutants (Total: 1, Killed: 0, Survived: 1)
602: eval { $data = decode_json($data) }; 603: if($@) {
Mutants (Total: 1, Killed: 0, Survived: 1)
604: undef $data; 605: } 606: } else { 607: undef $data; 608: } 609: if($data) {
Mutants (Total: 1, Killed: 0, Survived: 1)
610: $self->{'type'} = 'JSON'; 611: } 612: } else { 613: undef $data; 614: } 615: if(!$data) {

Mutants (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) {

Mutants (Total: 1, Killed: 0, Survived: 1)
646: $self->{'type'} = 'YAML'; 647: } 648: } 649: if((!$data) || (ref($data) ne 'HASH')) {

Mutants (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) {

Mutants (Total: 1, Killed: 0, Survived: 1)
657: $self->{'type'} = 'INI'; 658: } 659: } 660: if((!$data) || (ref($data) ne 'HASH')) {

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

661: # Maybe XML without the leading XML header 662: if($self->_load_driver('XML::Simple', ['XMLin'])) {

Mutants (Total: 1, Killed: 0, Survived: 1)
663: eval { $data = XMLin($path, ForceArray => 0, KeyAttr => []) }; 664: } 665: if((!$data) || (ref($data) ne 'HASH')) {
Mutants (Total: 1, Killed: 0, Survived: 1)
666: if($self->_load_driver('Config::Abstract')) {
Mutants (Total: 1, Killed: 0, Survived: 1)
667: # 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)
674: undef $data; 675: } elsif($data) { 676: $data = $data->get_all_settings(); 677: if(scalar(keys %{$data}) == 0) {
Mutants (Total: 2, Killed: 0, Survived: 2)
678: undef $data; 679: } 680: } 681: $self->{'type'} = 'Perl'; 682: } 683: } 684: if((!$data) || (ref($data) ne 'HASH')) {

Mutants (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()) {

Mutants (Total: 1, Killed: 0, Survived: 1)
688: $self->{'type'} = $ca->format(); 689: } 690: } 691: } 692: } 693: } 694: }; 695: if($logger) {

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

696: if($@) {

Mutants (Total: 1, Killed: 0, Survived: 1)
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: 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'}) {

Mutants (Total: 1, Killed: 0, Survived: 1)
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: 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'})) {

Mutants (Total: 1, Killed: 0, Survived: 1)
784: if($self->_load_data_reuse()) {
Mutants (Total: 1, Killed: 0, Survived: 1)
785: if(ref($ref) eq 'HASH') {
Mutants (Total: 1, Killed: 0, Survived: 1)
786: if(!tied %$ref) {
Mutants (Total: 1, Killed: 0, Survived: 1)
787: # 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: 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});

Mutants (Total: 2, Killed: 0, Survived: 2)
814: return 0 if($self->{reuse_failed});

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

815: 816: eval { 817: require Data::Reuse; 818: Data::Reuse->import(); 819: }; 820: if($@) {

Mutants (Total: 1, Killed: 0, Survived: 1)
821: # Cache the failure so we do not attempt to load again 822: $self->{reuse_failed} = 1; 823: return 0;

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

824: } โ—[NOT COVERED] 825 โ†’ 826 โ†’ 0 825: $self->{reuse_loaded} = 1; 826: return 1;

Mutants (Total: 2, Killed: 0, Survived: 2)
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: 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}) {

Mutants (Total: 1, Killed: 0, Survived: 1)
1021: return $data->{$key} if(exists $data->{$key});
Mutants (Total: 2, Killed: 0, Survived: 2)
1022: } 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: 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__