Object-Configure

 view release on metacpan or  search on metacpan

lib/Object/Configure.pm  view on Meta::CPAN

      # Inherits retries: 3 and log_level: info from parent

    # Result: Child class gets timeout=60, retries=3, log_level=info

Parent configuration files are optional.
If a parent class's configuration file doesn't exist, the module simply skips it and continues up the inheritance chain.
All discovered configuration files are tracked in the C<_config_files> array for hot reload support.

=head3 UNIVERSAL CONFIGURATION

All Perl classes implicitly inherit from C<UNIVERSAL>.
C<Object::Configure> takes advantage of this to provide a mechanism for universal configuration settings
that apply to all classes by default.

If you create a configuration file named C<universal.yml> (or C<universal.conf>, C<universal.json>, etc.)
in your configuration directory,
the settings in its C<UNIVERSAL> section will be inherited by all classes that use C<Object::Configure>,
unless explicitly overridden by class-specific configuration files.

This is particularly useful for setting application-wide defaults such as logging levels,
timeout values,
or other common parameters that should apply across all modules.

Example C<~/.conf/universal.yml>:

    ---
    UNIVERSAL:
      timeout: 30
      retries: 3
      logger:
        level: info

With this universal configuration file in place,
all classes will inherit these default values.
Individual classes can override any of these settings in their own configuration files:

Example C<~/.conf/my-special-class.yml>:

    ---
    My__Special__Class:
      timeout: 120
      # Inherits retries: 3 and logger.level: info from UNIVERSAL

The universal configuration is loaded first in the inheritance chain,
followed by parent class configurations,
and finally the specific class configuration,
with later configurations overriding earlier ones.

=head2 CHANGING BEHAVIOUR AT RUN TIME

=head3 USING A CONFIGURATION FILE

To control behavior at runtime, C<Object::Configure> supports loading settings from a configuration file via L<Config::Abstraction>.

A minimal example of a config file (C<~/.conf/local.conf>) might look like:

   [My__Module]
   logger.file = /var/log/mymodule.log

The C<configure()> function will read this file,
overlay it onto your default parameters,
and initialize the logger accordingly.

If the file is not readable and no config_dirs are provided,
the module will throw an error.
To be clear, in this case, inheritance is not followed.

This mechanism allows dynamic tuning of logging behavior (or other parameters you expose) without modifying code.

More details to be written.

=head3 USING ENVIRONMENT VARIABLES

C<Object::Configure> also supports runtime configuration via environment variables,
without requiring a configuration file.

Environment variables are read automatically when you use the C<configure()> function,
thanks to its integration with L<Config::Abstraction>.
These variables should be prefixed with your class name, followed by a double colon.

For example, to enable syslog logging for your C<My::Module> class,
you could set:

    export My__Module__logger__file=/var/log/mymodule.log

This would be equivalent to passing the following in your constructor:

     My::Module->new(logger => Log::Abstraction->new({ file => '/var/log/mymodule.log' });

All environment variables are read and merged into the default parameters under the section named after your class.
This allows centralized and temporary control of settings (e.g., for production diagnostics or ad hoc testing) without modifying code or files.

Note that environment variable settings take effect regardless of whether a configuration file is used,
and are applied during the call to C<configure()>.

More details to be written.

=head2 HOT RELOAD

Hot reload is not supported on Windows.

=head3 Basic Hot Reload Setup

    package My::App;
    use Object::Configure;

    sub new {
        my $class = shift;
        my $params = Object::Configure::configure($class, @_ ? \@_ : undef);
        my $self = bless $params, $class;

        # Register for hot reload
        Object::Configure::register_object($class, $self) if $params->{_config_file};

        return $self;
    }

    # Optional: Define a reload hook
    sub _on_config_reload {
        my ($self, $new_config) = @_;
        print "My::App config was reloaded!\n";

lib/Object/Configure.pm  view on Meta::CPAN

		$config_file = $obj->{_config_file} || $obj->{config_file};
	}

	return unless $config_file && -f $config_file;

	my $config = Config::Abstraction->new(
		config_file => $config_file,
		env_prefix  => "${class}__"
	);

	if($config) {
		my $new_params = $config->merge_defaults(
			defaults => {},
			section  => $class,
			merge    => 1,
			deep     => 1
		);

		foreach my $key (keys %$new_params) {
			next if $key =~ /^_/;

			if($key eq 'logger') {
				# Only the exact 'logger' key triggers logger reconstruction.
				# Keys like 'logger.file' are flat config values, not logger specs.
				my $val = $new_params->{$key};
				if(ref($val) || (defined($val) && $val ne $LOGGER_NULL)) {
					_reconfigure_logger($obj, $key, $val);
				} else {
					$obj->{$key} = $val;
				}
			} else {
				$obj->{$key} = $new_params->{$key};
			}
		}

		$obj->_on_config_reload($new_params) if $obj->can('_on_config_reload');

		$obj->{logger}->info("Configuration reloaded for $original_class")
			if $obj->{logger} && $obj->{logger}->can('info');
	}

	return;
}

# Purpose:   Replace the logger on an already-constructed object with one
#            built from a new config value (typically a YAML hashref).
#            Delegates to _build_logger so logger-creation logic lives in one place.
# Entry:     $obj is a blessed hashref. $key is the hash key to update (usually 'logger').
#            $logger_config is the new spec from the config file.
# Exit:      Returns nothing; updates $obj->{$key} in-place.
# Side:      May allocate a new Log::Abstraction instance.
sub _reconfigure_logger
{
	my ($obj, $key, $logger_config) = @_;
	my $carp_on_warn = $obj->{carp_on_warn} || 0;
	$obj->{$key} = _build_logger($logger_config, $carp_on_warn);
	return;
}

# Purpose:   Right-precedence deep merge of two hash references.
#            Scalar/arrayref values in $overlay replace those in $base entirely;
#            nested hashrefs are merged recursively.
# Entry:     Both args should be hashrefs (or undef/non-ref, handled gracefully).
# Exit:      Returns a new hashref; neither input is modified.
sub _deep_merge {
	my ($base, $overlay) = @_;

	return $overlay unless ref($base)    eq 'HASH';
	return $overlay unless ref($overlay) eq 'HASH';

	my $result = { %$base };

	foreach my $key (keys %$overlay) {
		if(ref($overlay->{$key}) eq 'HASH' && ref($result->{$key}) eq 'HASH') {
			$result->{$key} = _deep_merge($result->{$key}, $overlay->{$key});
		} else {
			$result->{$key} = $overlay->{$key};
		}
	}

	return $result;
}

# Clean up the watcher child and restore signal state on interpreter exit.
END {
	disable_hot_reload();
	restore_signal_handlers();
}

=head1 SEE ALSO

=over 4

=item * L<Config::Abstraction>

=item * L<Log::Abstraction>

=item * L<Test Dashboard|https://nigelhorne.github.io/Object-Configure/coverage/>

=back

=head1 LIMITATIONS

=over 4

=item * B<Global singleton state.> C<%_object_registry>, C<%_config_watchers>, and
C<%_config_file_stats> are package globals.  Two independent subsystems in the same
process share one hot-reload registry and one SIGUSR1 handler.  There is no
instance-level isolation.  A proper fix would wrap state in an object and allow
multiple independent C<Object::Configure> instances, but that would break the
existing constructor-call API (C<configure($class, \%params)>).

=item * B<Hot reload is Unix-only.> SIGUSR1 does not exist on Windows.
All signal-related paths are guarded with C<$^O ne 'MSWin32'>, so the module
loads on Windows but silently skips hot-reload registration.

=item * B<configure() is a God function.> At ~120 lines it handles arg validation,
config-file discovery, MRO walking, multi-file merging, env-var merging, logger
creation, and hot-reload bookkeeping.  Future versions should decompose this into
smaller, independently testable units.

=item * B<_deep_merge reimplements CPAN.> L<Hash::Merge::Simple> or L<Hash::Merge>
provide tested, feature-complete deep merge.  The internal C<_deep_merge> is 15
lines and correct for the current use, but does not handle arrayrefs (they are
replaced wholesale, not merged).  If array-merge semantics are ever needed, switch
to a CPAN module.

=item * B<No encapsulation enforcement.> Private helpers (C<_build_logger>,
C<_get_inheritance_chain>, etc.) are accessible to any caller.  L<Sub::Private>
(enforce mode) would make accidental external use a compile-time error.  It is not
added here to avoid a smoker dependency on a less-common module.

=item * B<configure() signature is positional, instantiate() is named.>  The two
public constructors have inconsistent calling conventions.  Normalising them to named
args would require a deprecation cycle.

=item * B<mro::get_linear_isa and UNIVERSAL.>  Perl's C<mro::get_linear_isa> does



( run in 1.474 second using v1.01-cache-2.11-cpan-7fcb06a456a )