—package
Kelp::Module::Config;
use
Carp;
use
Try::Tiny;
use
Path::Tiny;
use
Kelp::Util;
# Extension to look for
attr
ext
=>
'pl'
;
# Default modes to be processed before the app mode
attr
default_modes
=>
sub
{ [
qw(config)
] };
# Directory where config files are
attr
path
=>
sub
{
my
$self
=
shift
;
return
[
$ENV
{KELP_CONFIG_DIR},
'conf'
,
$self
->app->path,
$self
->app->path .
'/conf'
,
$self
->app->path .
'/../conf'
];
};
attr
separator
=>
sub
{
quotemeta
'.'
};
# Defaults
attr
data
=>
sub
{
my
$self
=
shift
;
# Return a big hash with default values
return
{
# Default charset is UTF-8
charset
=>
'UTF-8'
,
request_charset
=>
'UTF-8'
,
# Modules to load
modules
=> [
qw/JSON Template/
],
# Encoders
encoders
=> {
json
=> {
internal
=> {
utf8
=> 0,
},
},
},
# Module initialization params
modules_init
=> {
# Routes
Routes
=> {
base
=> (
$self
->app->can(
'_real_class'
) ?
$self
->app->_real_class :
ref
$self
->app),
},
# Template
Template
=> {
paths
=> [
$self
->app->path .
'/views'
,
$self
->app->path .
'/../views'
]
},
# JSON
JSON
=> {
allow_blessed
=> 1,
convert_blessed
=> 1,
utf8
=> 1
},
},
# List of the middleware to add
middleware
=> [],
# Initializations of the middleware
middleware_init
=> {},
};
};
sub
get
{
my
(
$self
,
$path
,
$default
) =
@_
;
return
unless
$path
;
my
$val
=
$self
->data;
for
my
$chunk
(
split
(
$self
->separator,
$path
)) {
return
$default
unless
exists
$val
->{
$chunk
};
croak
"Config path $path breaks at '$chunk'"
unless
ref
$val
eq
'HASH'
;
$val
=
$val
->{
$chunk
};
}
return
$val
;
}
# Override this one to use other config formats.
sub
load
{
my
(
$self
,
$filename
) =
@_
;
# Open and read file
my
$text
;
try
{
$text
= path(
$filename
)->slurp({
binmode
=>
':encoding(UTF-8)'
});
};
if
(!
defined
$text
) {
warn
"Can not read config file "
.
$filename
;
return
{};
}
my
(
$hash
,
$error
);
{
local
$@;
my
$app
=
$self
->app;
my
$module
=
$filename
;
$module
=~ s/\W/_/g;
$hash
=
eval
"package Kelp::Module::Config::Sandbox::$module;"
.
"use Kelp::Base -strict;"
.
"sub app; local *app = sub { \$app };"
.
"sub include(\$); local *include = sub { \$self->load(\@_) };"
.
$text
;
$error
= $@;
}
die
"Config file $filename parse error: "
.
$error
if
$error
;
die
"Config file $filename did not return a HASH - $hash"
unless
ref
$hash
eq
'HASH'
;
return
$hash
;
}
sub
process_mode
{
my
(
$self
,
$mode
) =
@_
;
my
$filename
=
sub
{
my
@paths
=
ref
(
$self
->path) ? @{
$self
->path} : (
$self
->path);
for
my
$path
(
@paths
) {
next
unless
defined
$path
;
my
$filename
=
sprintf
(
'%s/%s.%s'
,
$path
,
$mode
,
$self
->ext);
return
$filename
if
-r
$filename
;
}
}
->();
unless
(
$filename
) {
if
(
$ENV
{KELP_CONFIG_WARN}) {
my
$message
=
$mode
eq
'config'
?
"Main config file not found or not readable"
:
"Config file for mode '$mode' not found or not readable"
;
warn
$message
;
}
return
;
}
my
$parsed
= {};
try
{
$parsed
=
$self
->load(
$filename
);
}
catch
{
die
"Parsing $filename died with error: '${_}'"
;
};
$self
->data(
$self
->merge(
$self
->data,
$parsed
));
}
sub
build
{
my
(
$self
,
%args
) =
@_
;
# Find, parse and merge 'config' and mode files
for
my
$name
(@{
$self
->default_modes},
$self
->app->mode) {
$self
->process_mode(
$name
);
}
# Undocumented! Add 'extra' argument to unlock these special features:
# 1. If the extra argument contains a HASH, it will be merged to the
# configuration upon loading.
# 2. A new attribute '_cfg' will be registered into the app, which has
# three methods: merge, clear and set. Use them to merge a hash into
# the configuration, clear it, or set it to a new value. You can do those
# at any point in the life of the app.
#
if
(
my
$extra
=
delete
$args
{extra}) {
$self
->data(
$self
->merge(
$self
->data,
$extra
))
if
ref
(
$extra
) eq
'HASH'
;
$self
->register(
# A tiny object containing only merge, clear and set. Very useful when
# you're writing tests and need to add new config options, set the
# entire config hash to a new value, or clear it completely.
_cfg
=> Plack::Util::inline_object(
merge
=>
sub
{
$self
->data(
$self
->merge(
$self
->data,
$_
[0]));
},
clear
=>
sub
{
$self
->data({}) },
set
=>
sub
{
$self
->data(
$_
[0]) }
)
);
}
$self
->register(
# Return the entire config hash
config_hash
=>
$self
->data,
# A wrapper arount the get method
config
=>
sub
{
my
$app
=
shift
;
return
$self
->get(
@_
);
}
);
}
sub
merge
{
my
(
$self
,
@args
) =
@_
;
return
_merge(
@args
);
}
# backcompat
sub
_merge
{
goto
\
&Kelp::Util::merge
;
}
1;
__END__
=pod
=head1 NAME
Kelp::Module::Config - Configuration for Kelp applications
=head1 DESCRIPTION
This is one of the two modules that are automatically loaded for each and every
Kelp application. The other one is L<Kelp::Module::Routes>. It reads
configuration files containing Perl-style hashes, and merges them depending on
the value of the application's C<mode> attribute.
The main configuration file name is C<config.pl>, and it will be searched in
the C<conf> and C<../conf> directories. You can also set the C<KELP_CONFIG_DIR>
environmental variable with the path to the configuration files.
This module comes with some L<default values|/DEFAULTS>, so if there are no
configuration files found, those values will be used. Any values from
configuration files will add to or override the default values.
=head1 ORDER
First the module will look for C<conf/config.pl>, then for
C<../conf/config.pl>. If found, they will be parsed and merged into the
default values. The same order applies to the I<mode> file too, so if the
application L<mode|Kelp/mode> is I<development>, then C<conf/development.pl>
and C<../conf/development.pl> will be looked for. If found, they will also be
merged to the config hash.
=head1 ACCESSING THE APPLICATION
The application instance can be accessed within the config files via the C<app>
keyword.
{
bin_path => app->path . '/bin'
}
=head1 INCLUDING FILES
To include other config files, one may use the C<include> keyword.
# config.pl
{
modules_init => {
Template => include('conf/my_template.pl')
}
}
# my_template.pl
{
path => 'views/',
utf8 => 1
}
Any config file may be included as long as it returns a hashref.
=head1 MERGING
The first configuration file this module will look for is C<config.pl>. This is
where you should keep configuration options that apply to all running
environments. The mode-specific configuration file will be merged to this
config, and it will take priority. Merging is done as follows:
=over
=item Scalars will always be overwritten.
=item Hashes will be merged.
=item Arrays will be overwritten, except in case when the name of the array contains a
sigil as follows:
=over
=item
C<+> in front of the name will add the elements to the array:
# in config.pl
{
middleware => [qw/Bar Foo/]
}
# in development.pl
{
'+middleware' => ['Baz'] # Add 'Baz' in development
}
=item
C<-> in front of the name will remove the elements from the array:
# in config.pl
{
modules => [qw/Template JSON Logger/]
}
# in test.pl
{
'-modules' => [qw/Logger/] # Remove the Logger modules in test mode
}
=item
No sigil will cause the array to be completely replaced:
# in config.pl
{
middleware => [qw/Bar Foo/]
}
# in cli.pl
{
middleware => [] # No middleware in CLI
}
=back
Note that the merge sigils only apply to arrays. All other types will keep the
sigil in the key name:
# config.pl
{
modules => ["+MyApp::Fully::Qualified::Name"],
modules_init => {
"+MyApp::Fully::Qualified::Name" => { opt1 => 1, opt2 => 2 }
}
}
# development.pl
{
modules_init => {
"+MyApp::Fully::Qualified::Name" => { opt3 => 3 }
}
}
=back
=head1 REGISTERED METHODS
This module registers the following methods into the underlying app:
=head2 config
A wrapper for the L</get> method.
# Somewhere in the app
my $pos = $self->config('row.col.position');
# Gets {row}->{col}->{position} from the config hash
my $hello = $self->config('hello', 'world');
# gets {hello} from the config hash and returns 'world' if not found
=head2 config_hash
A reference to the entire configuration hash.
my $pos = $self->config_hash->{row}->{col}->{position};
Using this or L</config> is entirely up to the application developer.
=head3 _cfg
A tiny object that contains only three methods - B<merge>, B<clear> and B<set>.
It allows you to merge values to the config hash, clear it completely or
set it to an entirely new value. This method comes handy when writing tests.
# Somewhere in a .t file
my $app = MyApp->new( mode => 'test' );
my %original_config = %{ $app->config_hash };
$app->_cfg->merge( { middleware => ['Foo'] } );
# Now you can test with middleware Foo added to the config
# Revert to the original configuration
$app->_cfg->set( \%original_config );
=head1 ATTRIBUTES
This module implements some attributes, which can be overridden by subclasses.
=head2 ext
The file extension of the configuration files. Default is C<pl>.
=head2 default_modes
An array reference of modes to be processed before the application's mode.
Default is C<['config']>.
=head2 separator
A regular expression for the value separator used by L</get>. The default is
C<qr/\./>, i.e. a dot.
=head2 path
Specifies a path, or an array of paths where to look for configuration files.
This is particularly useful when writing tests, because you can set a custom
path to a peculiar configuration.
=head2 data
The hashref with data contained in all of the merged configurations.
=head1 METHODS
The module also implements some methods for parsing the config files, which can
be overridden in extending classes.
=head2 get
C<get($string)>
C<get($string, $default)>
Get a value from the config using a separated string.
my $value = $c->get('bar.foo.baz');
my $same = $c->get('bar')->{foo}->{baz};
my $again = $c->data->{bar}->{foo}->{baz};
By default the separator is a dot, but this can be changed via the
L</separator> attribute.
If it doesn't find the requested value, C<$default> will be returned (or undef
if not passed). If along the way it finds a different type that C<HASH> (for
example you requested C<a.b>, but C<a> is a string) then an exception will be
raised.
=head2 load
C<load(filename)>
Loads, and parses the file C<$filename> and returns a hash reference.
=head2 process_mode
C<process_mode($mode)>
Finds the file (if it exists) corresponding to C<$mode>, parses it and merges
it into the data. Useful, when you want to process and extra config file during
the application initialization.
# lib/MyApp.pm
sub build {
$self->loaded_modules->{Config}->process_mode( 'more_config' );
}
=head1 DEFAULTS
This module sets certain default values. All of them may be overridden in any of
the C<conf/> files. It probably pays to view the code of this module and look
and the C<defaults> sub to see what is being set by default, but here is the
short version:
=head2 charset
Application's charset, which it will by default use to encode the body of the
response (unless charset is set manually for a response). Any encoding
supported by L<Encode> is fine. It should probably stay as default C<UTF-8>
unless you're doing something non-standard.
Can be set to undef to disable response encoding.
=head2 request_charset
Default incoming charset, which will be used to decode requests (unless the
request contains its own). It will always be used to decode URI elements of the
request. It is B<strongly recommended> this stays as default C<UTF-8>, but can
also be set to other one-byte encodings if needed.
Can be set to undef to disable request decoding entirely.
=head2 app_url
Abosulte URL under which the application is available.
=head2 encoders
A hashref of extra encoder configs to be used by L<Kelp/get_encoder>. By
default, only C<encoders.json.internal> is defined and disables C<utf8> flag of
the JSON module.
=head2 modules
An arrayref with module names to load on startup. The default value is
C<['JSON', 'Template']>
=head2 modules_init
A hashref with initializations for each of the loaded modules, except this one,
ironically.
=head2 middleware
An arrayref with middleware to load on startup. The default value is an
empty array.
=head2 middleware_init
A hashref with initialization arguments for each of the loaded middleware.
=head1 SUBCLASSING
You can subclass this module and use other types of configuration files
(for example YAML). You need to override the C<ext> attribute
and the C<load> subroutine.
package Kelp::Module::Config::Custom;
use Kelp::Parent 'Kelp::Module::Config';
# Set the config file extension to .cus
attr ext => 'cus';
sub load {
my ( $self, $filename ) = @_;
# Load $filename, parse it and return a hashref
}
1;
Later ...
# app.psgi
use MyApp;
my $app = MyApp->new( config_module => 'Config::Custom' );
run;
The above example module will look for C<config/*.cus> to load as configuration.
=head1 TESTING
Since the config files are searched in both C<conf/> and C<../conf/>, you can
use the same configuration set of files for your application and for your tests.
Assuming the all of your test will reside in C<t/>, they should be able to load
and find the config files at C<../conf/>.
=head1 ENVIRONMENT VARIABLES
=head2 KELP_CONFIG_WARN
This module will not warn for missing config and mode files. It will
silently load the default configuration hash. Set KELP_CONFIG_WARN to a
true value to make this module warn about missing files.
$ KELP_CONFIG_WARN=1 plackup app.psgi
=cut