Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
784cf27
Move Role::ConfigReader to Role::Config
mikkoi Feb 2, 2022
0f6e4d2
Create Role::Config to be the "config" part of obj
mikkoi Feb 2, 2022
50e40ab
Create ConfigReader role and an implementation
mikkoi Feb 2, 2022
a852aad
Add tests for new ConfigReader
mikkoi Feb 2, 2022
c4d0e97
Change regex: remove POSIX form
mikkoi Mar 27, 2022
adb70a7
Reformat
mikkoi Mar 27, 2022
9403209
Reformat: no warnings 'once'
mikkoi Mar 27, 2022
82b23b7
Rework POD and add an example how to extend
mikkoi Mar 27, 2022
e34deac
Rewrite documentation of Dancer2::Core::Role::Config
mikkoi Mar 27, 2022
7b815da
Add ABSTRACT to ConfigReader::File::Simple
mikkoi Mar 27, 2022
c8dbe9a
Remove attr config_files from Dancer2::Core::Role::Config
mikkoi Mar 27, 2022
af57075
Create Role::HasEnvironment and tests
mikkoi May 8, 2022
ad468e9
Add more logging to ConfigReader::File::Simple
mikkoi May 8, 2022
984b95d
Add isa type to HasLocation::location
mikkoi May 8, 2022
06042eb
Separate normalizers from Config to ConfigUtils
mikkoi May 8, 2022
38dd888
Split Role::Config to ConfigReader and Role::HasConfig
mikkoi May 8, 2022
53dfb01
Add Mikko Koivunalho to contributors list
mikkoi May 8, 2022
199e30c
Cleanup code
mikkoi May 12, 2022
93613d8
Fix documentation in File::Simple
mikkoi May 12, 2022
7bc1194
Fix test to use File::Simple->config_location
mikkoi May 12, 2022
74e28c4
Fix documentation in Role::ConfigReader
mikkoi May 12, 2022
b06a7bc
Remove whitespace
mikkoi Aug 21, 2022
4a62c1b
Add Mikko Koivunalho to contributors
mikkoi Sep 10, 2022
c30f818
Change DANCER_CONFIG_READERS separator
mikkoi Jun 15, 2025
0738b39
allow the ConfigReaders to bootstrap
yanick Jun 24, 2025
1cf0d60
Merge remote-tracking branch 'origin/main' into feature/new-config-sy…
yanick Jul 12, 2025
03955b8
add Test::Exception to the test requirements
yanick Jul 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ We are also on IRC: #dancer on irc.perl.org.
Michael Kröll
Michał Wojciechowski
Mike Katasonov
Mikko Koivunalho
Mohammad S Anwar
mokko
Nick Patch
Expand Down
1 change: 1 addition & 0 deletions cpanfile
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ test_requires 'Test::EOL';
test_requires 'Test::Fatal';
test_requires 'Test::More';
test_requires 'Test::More', '0.92';
test_requires 'Test::Exception';

author_requires 'Test::NoTabs';
author_requires 'Test::Pod';
Expand Down
10 changes: 10 additions & 0 deletions lib/Dancer2/Config.pod
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ PSGI middleware. The default middleware wrappers are:

=back

=head3 additional_config_readers

L<Dancer2::ConfigReader> instances to bootstrap the application's
configuration. See the ConfigReader's POD for more information.

=head2 Content type / character set

=head3 content_type (string)
Expand Down Expand Up @@ -597,6 +602,11 @@ Sets the configuration directory.

This correlates to the C<confdir> config option.

=head2 DANCER_CONFIG_VERBOSE

Outputs a lot of debugging information when generating the configuration of
the application.
Comment on lines +607 to +608

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we mean "generating" here, or "reading"?


=head3 DANCER_ENVDIR

Sets the environment directory.
Expand Down
127 changes: 94 additions & 33 deletions lib/Dancer2/ConfigReader.pm
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ use Config::Any;
use Hash::Merge::Simple;
use Carp 'croak';
use Module::Runtime qw{ use_module };
use Ref::Util qw/ is_arrayref /;
use Scalar::Util qw/ blessed /;

use Dancer2::Core::Factory;
use Dancer2::Core;
use Dancer2::Core::Types;
use Dancer2::ConfigUtils 'normalize_config_entry';

our $MAX_CONFIGS = $ENV{DANCER_MAX_CONFIGS} || 100;

has location => (
is => 'ro',
isa => Str,
Expand Down Expand Up @@ -71,17 +75,59 @@ has config_readers => (
sub _build_config {
my ($self) = @_;

my $default = $self->default_config;
my $config = Hash::Merge::Simple->merge(
$default,
map {
warn "Merging config from @{[ $_->name() ]}\n" if $ENV{DANCER_CONFIG_VERBOSE};
$_->read_config()
} @{ $self->config_readers }
);
my $config = $self->default_config;

$config = $self->_normalize_config($config);
return $config;
my $nbr_config = 0;

my @readers = @{ $self->config_readers };

my $config_to_object = sub {
my $thing = $_;

return $thing if blessed $thing;

$thing = { $thing => {} } unless ref $thing;

die "additional_config_readers entry can have only one key\n"
if 1 < keys %$thing;
Comment on lines +91 to +92

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be a bit more readable as if keys %$thing != 1, and it would also catch any case where we were passed an empty hashref too.

Suggested change
die "additional_config_readers entry can have only one key\n"
if 1 < keys %$thing;
die "additional_config_readers entry must have exactly one key\n"
if keys %$thing != 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Yanick is AFK for the next few weeks, so I will update and merge today.


my( $class, $args ) = %$thing;

return use_module($class)->new(
location => $self->location,
environment => $self->environment,
%$args,
);
};

while( my $r = shift @readers ) {
die <<"END" if $nbr_config++ >= $MAX_CONFIGS;
MAX_CONFIGS exceeded: read over $MAX_CONFIGS configurations

Looks like you have an infinite recursion in your configuration system.
Re-run with DANCER_CONFIG_VERBOSE=1 to see what is going on.

If your application really read that many configs (may \$dog have mercy
on your soul), you can increase the limit via the environment variable
DANCER_MAX_CONFIGS.

END
warn "Reading config from @{[ $r->name() ]}\n" if $ENV{DANCER_CONFIG_VERBOSE};
my $local_config = $r->read_config;

if( my $additionals = delete $local_config->{additional_config_readers} ) {

warn "Additional config readers found\n" if $ENV{DANCER_CONFIG_VERBOSE};

unshift @readers, map { $config_to_object->($_) } is_arrayref($additionals) ? @$additionals : ($additionals);
}

$config = Hash::Merge::Simple->merge(
$config, $local_config
);
}

return $self->_normalize_config($config);
}

sub _normalize_config {
Expand Down Expand Up @@ -116,26 +162,18 @@ __END__

=head1 DESCRIPTION

This class provides a C<config> attribute that - when accessing
the first time - feeds itself by executing one or more
B<ConfigReader> packages.
This class provides a C<config> attribute that
is populated by executing one or more B<ConfigReader> packages.
The default ConfigReader used by default is C<Dancer2::ConfigReader::Config::Any>.
Comment on lines +165 to +167

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly less tautology:

Suggested change
This class provides a C<config> attribute that
is populated by executing one or more B<ConfigReader> packages.
The default ConfigReader used by default is C<Dancer2::ConfigReader::Config::Any>.
This class provides a C<config> attribute which is populated by executing
one or more B<ConfigReader> packages.
The default ConfigReader used is L<Dancer2::ConfigReader::Config::Any>.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better! I will make this change and merge today.


Also provides a C<setting()> method which is supposed to be used by externals to
read/write config entries.

You can control which B<ConfigReader>
class or classes to use to create the config.

Use C<DANCER_CONFIG_READERS> environment variable to define
which class or classes you want.
If more than one config reader is used, their configurations are merged
in left-to-write order where the previous config items get overwritten by subsequent ones.

DANCER_CONFIG_READERS='Dancer2::ConfigReader::Config::Any,Dancer2::ConfigReader::CustomConfig'

If you want several, separate them with a comma (",").
Configs are added in left-to-write order where the previous
config items get overwritten by subsequent ones.

For example, if config
For example, assuming we are using 3 config readers,
if the first config reader returns

item1: content1
item2: content2
Expand All @@ -149,7 +187,7 @@ For example, if config
subitem1: subcontent1
subitem2: subcontent2

was followed by config
and the second returns

item2: content9
item3:
Expand All @@ -161,7 +199,7 @@ was followed by config
subsubitem5: subsubcontent5
item4: content4

then the final config would be
then the final config is

item1: content1
item2: content9
Expand All @@ -175,19 +213,42 @@ then the final config would be
subsubitem5: subsubcontent5
item4: content4

The default B<ConfigReader> is C<Dancer2::ConfigReader::Config::Any>.
=head2 Configuring the ConfigReaders via DANCER_CONFIG_READERS

You can control which B<ConfigReader>
class or classes to use to create the config
via the C<DANCER_CONFIG_READERS> environment.

DANCER_CONFIG_READERS='Dancer2::ConfigReader::Config::Any,Dancer2::ConfigReader::CustomConfig'

If you want several, separate them with a comma (",").

=head2 Bootstrapping the ConfigReaders via C<additional_config_readers>

If the key C<additional_config_readers> is found in one in one or more of the configurations provided by the ConfigReaders, it'll be
instantiated and added to the list of configurations to merge. This way you can, for example, create a basic F<config.yml> that is

additional_config_readers:
- Dancer2::ConfigReader::SQLite:
path: /path/to/sqlite.db
table: config

The default ConfigReader L<Dancer2::ConfigReader::::Config::Any> will pick that file and proceed to instantiate C<Dancer2::ConfigReader::SQLite>
with the provided parameters.

C<additional_config_readers> can take one or a list of reader configurations, which can be either the name of the ConfigReader's class, or the
key/value pair of the class name and its constructor's arguments.

You can also create your own custom B<ConfigReader> classes.
=head2 Creating your own custom B<ConfigReader> classes.

If you want, you can also extend class C<Dancer2::ConfigReader::Config::Any>.
Here is an example:
Here's an example extending class C<Dancer2::ConfigReader::Config::Any>.

package Dancer2::ConfigReader::FileExtended;
package Dancer2::ConfigReader::Config::Any::Extended;
use Moo;
extends 'Dancer2::ConfigReader::Config::Any';
has name => (
is => 'ro',
default => sub {'FileExtended'},
default => sub {'Config::Any::Extended'},
);
around read_config => sub {
my ($orig, $self) = @_;
Expand Down
30 changes: 22 additions & 8 deletions t/config_many.t
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,33 @@ BEGIN {
# undefine ENV vars used as defaults for app environment in these tests
local $ENV{DANCER_ENVIRONMENT};
local $ENV{PLACK_ENV};
$ENV{DANCER_CONFIG_READERS}
= 'Dancer2::ConfigReader::Config::Any,Dancer2::ConfigReader::TestDummy';
$ENV{DANCER_CONFDIR} = './t/app/t1';
}
use lib '.';
use lib './t/lib';

use t::app::t1::lib::App1;
use Dancer2::Core::App;

subtest basic => sub {
$ENV{DANCER_CONFIG_READERS}
= 'Dancer2::ConfigReader::Config::Any,Dancer2::ConfigReader::TestDummy';
my $app = Dancer2::Core::App->new( name => 'basic' );

is $app->config->{app}->{config}, 'ok',
$app->name . ": config loaded properly";
is $app->config->{dummy}->{dummy_subitem}, 2,
$app->name . ": dummy config loaded properly";
};

subtest additional_config_readers => sub {
$ENV{DANCER_CONFIG_READERS} = 'Dancer2::ConfigReader::Additional';

my $app = Dancer2->runner->apps->[0];
my $app = Dancer2::Core::App->new( name => 'additional' );

is $app->config->{app}->{config}, 'ok',
$app->name . ": config loaded properly";
is $app->config->{dummy}->{dummy_subitem}, 2,
$app->name . ": dummy config loaded properly";
is $app->config->{app}->{config}, 'ok',
$app->name . ": config loaded properly";
is $app->config->{dummy}->{dummy_subitem}, 2,
$app->name . ": dummy config loaded properly";
};

done_testing;
28 changes: 21 additions & 7 deletions t/config_many_failure.t
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,38 @@ use strict;
use warnings;

use Test::More;
use Test::Exception;
use File::Spec;
use English;

use Dancer2::Core::App;

BEGIN {
# undefine ENV vars used as defaults for app environment in these tests
local $ENV{DANCER_ENVIRONMENT};
local $ENV{PLACK_ENV};
$ENV{DANCER_CONFIG_READERS}
= 'Dancer2::ConfigReader::Config::Any Dancer2::ConfigReader::TestDummy';
}

use lib q{.};
use lib './t/lib';

local $EVAL_ERROR = undef;
my $eval_r = eval 'use t::app::t1::lib::App1;';
my $eval_e = $EVAL_ERROR;
is $eval_r, undef, 'Eval failed correctly';
like $eval_e, qr{`Dancer2::ConfigReader::Config::Any Dancer2::ConfigReader::TestDummy' is not a module name}, 'Correct dying and error';
subtest 'bad DANCER_CONFIG_READERS' => sub {
# space instead of comma, ooops
$ENV{DANCER_CONFIG_READERS}
= 'Dancer2::ConfigReader::Config::Any Dancer2::ConfigReader::TestDummy';

throws_ok {
Dancer2::Core::App->new( name => 'basic' );
} qr{`Dancer2::ConfigReader::Config::Any Dancer2::ConfigReader::TestDummy' is not a module name};
};

subtest 'infinite loop of configs' => sub {
$ENV{DANCER_CONFIG_READERS}
= 'Dancer2::ConfigReader::Recursive';

throws_ok {
Dancer2::Core::App->new( name => 'basic' );
} qr{MAX_CONFIGS exceeded};
};

done_testing;
36 changes: 36 additions & 0 deletions t/lib/Dancer2/ConfigReader/Additional.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package Dancer2::ConfigReader::Additional;
use Moo;
use Dancer2::Core::Factory;
use Dancer2::Core;
use Dancer2::Core::Types;
use Dancer2::FileUtils 'path';

with 'Dancer2::Core::Role::ConfigReader';

has name => (
is => 'ro',
isa => Str,
lazy => 0,
default => 'Additional',
);

has config_files => (
is => 'ro',
lazy => 1,
isa => ArrayRef,
default => sub {
my ($self) = @_;
return [];
},
);

sub read_config {
return {
additional_config_readers => [qw/
Dancer2::ConfigReader::Config::Any
Dancer2::ConfigReader::TestDummy
/]
};
}

1;
34 changes: 34 additions & 0 deletions t/lib/Dancer2/ConfigReader/Recursive.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package Dancer2::ConfigReader::Recursive;
use Moo;
use Dancer2::Core::Factory;
use Dancer2::Core;
use Dancer2::Core::Types;
use Dancer2::FileUtils 'path';

with 'Dancer2::Core::Role::ConfigReader';

has name => (
is => 'ro',
isa => Str,
lazy => 0,
default => 'Recursive',
);

has config_files => (
is => 'ro',
lazy => 1,
isa => ArrayRef,
default => sub {
my ($self) = @_;
return [];
},
);

sub read_config {
return {
additional_config_readers =>
'Dancer2::ConfigReader::Recursive'
};
}

1;
Loading