Skip to content

Commit 0738b39

Browse files
committed
allow the ConfigReaders to bootstrap
1 parent c30f818 commit 0738b39

File tree

6 files changed

+217
-46
lines changed

6 files changed

+217
-46
lines changed

lib/Dancer2/Config.pod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,11 @@ PSGI middleware. The default middleware wrappers are:
185185

186186
=back
187187

188+
=head3 additional_config_readers
189+
190+
L<Dancer2::ConfigReader> instances to bootstrap the application's
191+
configuration. See the ConfigReader's POD for more information.
192+
188193
=head2 Content type / character set
189194

190195
=head3 content_type (string)
@@ -597,6 +602,11 @@ Sets the configuration directory.
597602

598603
This correlates to the C<confdir> config option.
599604

605+
=head2 DANCER_CONFIG_VERBOSE
606+
607+
Outputs a lot of debugging information when generating the configuration of
608+
the application.
609+
600610
=head3 DANCER_ENVDIR
601611

602612
Sets the environment directory.

lib/Dancer2/ConfigReader.pm

Lines changed: 93 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@ use Config::Any;
88
use Hash::Merge::Simple;
99
use Carp 'croak';
1010
use Module::Runtime qw{ use_module };
11+
use Ref::Util qw/ is_arrayref /;
12+
use Scalar::Util qw/ blessed /;
1113

1214
use Dancer2::Core::Factory;
1315
use Dancer2::Core;
1416
use Dancer2::Core::Types;
1517
use Dancer2::ConfigUtils 'normalize_config_entry';
1618

19+
our $MAX_CONFIGS = $ENV{DANCER_MAX_CONFIGS} || 100;
20+
1721
has location => (
1822
is => 'ro',
1923
isa => Str,
@@ -71,17 +75,59 @@ has config_readers => (
7175
sub _build_config {
7276
my ($self) = @_;
7377

74-
my $default = $self->default_config;
75-
my $config = Hash::Merge::Simple->merge(
76-
$default,
77-
map {
78-
warn "Merging config from @{[ $_->name() ]}\n" if $ENV{DANCER_CONFIG_VERBOSE};
79-
$_->read_config()
80-
} @{ $self->config_readers }
81-
);
78+
my $config = $self->default_config;
8279

83-
$config = $self->_normalize_config($config);
84-
return $config;
80+
my $nbr_config = 0;
81+
82+
my @readers = @{ $self->config_readers };
83+
84+
my $config_to_object = sub {
85+
my $thing = $_;
86+
87+
return $thing if blessed $thing;
88+
89+
$thing = { $thing => {} } unless ref $thing;
90+
91+
die "additional_config_readers entry can have only one key\n"
92+
if 1 < keys %$thing;
93+
94+
my( $class, $args ) = %$thing;
95+
96+
return use_module($class)->new(
97+
location => $self->location,
98+
environment => $self->environment,
99+
%$args,
100+
);
101+
};
102+
103+
while( my $r = shift @readers ) {
104+
die <<"END" if $nbr_config++ >= $MAX_CONFIGS;
105+
MAX_CONFIGS exceeded: read over $MAX_CONFIGS configurations
106+
107+
Looks like you have an infinite recursion in your configuration system.
108+
Re-run with DANCER_CONFIG_VERBOSE=1 to see what is going on.
109+
110+
If your application really read that many configs (may \$dog have mercy
111+
on your soul), you can increase the limit via the environment variable
112+
DANCER_MAX_CONFIGS.
113+
114+
END
115+
warn "Reading config from @{[ $r->name() ]}\n" if $ENV{DANCER_CONFIG_VERBOSE};
116+
my $local_config = $r->read_config;
117+
118+
if( my $additionals = delete $local_config->{additional_config_readers} ) {
119+
120+
warn "Additional config readers found\n" if $ENV{DANCER_CONFIG_VERBOSE};
121+
122+
unshift @readers, map { $config_to_object->($_) } is_arrayref($additionals) ? @$additionals : ($additionals);
123+
}
124+
125+
$config = Hash::Merge::Simple->merge(
126+
$config, $local_config
127+
);
128+
}
129+
130+
return $self->_normalize_config($config);
85131
}
86132

87133
sub _normalize_config {
@@ -116,26 +162,18 @@ __END__
116162
117163
=head1 DESCRIPTION
118164
119-
This class provides a C<config> attribute that - when accessing
120-
the first time - feeds itself by executing one or more
121-
B<ConfigReader> packages.
165+
This class provides a C<config> attribute that
166+
is populated by executing one or more B<ConfigReader> packages.
167+
The default ConfigReader used by default is C<Dancer2::ConfigReader::File::Simple>.
122168
123169
Also provides a C<setting()> method which is supposed to be used by externals to
124170
read/write config entries.
125171
126-
You can control which B<ConfigReader>
127-
class or classes to use to create the config.
128-
129-
Use C<DANCER_CONFIG_READERS> environment variable to define
130-
which class or classes you want.
131-
132-
DANCER_CONFIG_READERS='Dancer2::ConfigReader::File::Simple,Dancer2::ConfigReader::CustomConfig'
133-
134-
If you want several, separate them with a comma (",").
135-
Configs are added in left-to-write order where the previous
136-
config items get overwritten by subsequent ones.
172+
If more than one config reader is used, their configurations are merged
173+
in left-to-write order where the previous config items get overwritten by subsequent ones.
137174
138-
For example, if config
175+
For example, assuming we are using 3 config readers,
176+
if the first config reader returns
139177
140178
item1: content1
141179
item2: content2
@@ -149,7 +187,7 @@ For example, if config
149187
subitem1: subcontent1
150188
subitem2: subcontent2
151189
152-
was followed by config
190+
and the second returns
153191
154192
item2: content9
155193
item3:
@@ -161,7 +199,7 @@ was followed by config
161199
subsubitem5: subsubcontent5
162200
item4: content4
163201
164-
then the final config would be
202+
then the final config is
165203
166204
item1: content1
167205
item2: content9
@@ -175,12 +213,36 @@ then the final config would be
175213
subsubitem5: subsubcontent5
176214
item4: content4
177215
178-
The default B<ConfigReader> is C<Dancer2::ConfigReader::File::Simple>.
179216
180-
You can also create your own custom B<ConfigReader> classes.
217+
=head2 Configuring the ConfigReaders via DANCER_CONFIG_READERS
218+
219+
You can control which B<ConfigReader>
220+
class or classes to use to create the config
221+
via the C<DANCER_CONFIG_READERS> environment.
222+
223+
DANCER_CONFIG_READERS='Dancer2::ConfigReader::File::Simple,Dancer2::ConfigReader::CustomConfig'
224+
225+
If you want several, separate them with a comma (",").
226+
227+
=head2 Bootstrapping the ConfigReaders via C<additional_config_readers>
228+
229+
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
230+
instantiated and added to the list of configurations to merge. This way you can, for example, create a basic F<config.yml> that is
231+
232+
additional_config_readers:
233+
- Dancer2::ConfigReader::SQLite:
234+
path: /path/to/sqlite.db
235+
table: config
236+
237+
The default ConfigReader L<Dancer2::ConfigReader::File::Simple> will pick that file and proceed to instantiate C<Dancer2::ConfigReader::SQLite>
238+
with the provided parameters.
239+
240+
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
241+
key/value pair of the class name and its constructor's arguments.
242+
243+
=head2 Creating your own custom B<ConfigReader> classes.
181244
182-
If you want, you can also extend class C<Dancer2::ConfigReader::File::Simple>.
183-
Here is an example:
245+
Here's an example extending class C<Dancer2::ConfigReader::File::Simple>.
184246
185247
package Dancer2::ConfigReader::FileExtended;
186248
use Moo;

t/config_many.t

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,33 @@ BEGIN {
88
# undefine ENV vars used as defaults for app environment in these tests
99
local $ENV{DANCER_ENVIRONMENT};
1010
local $ENV{PLACK_ENV};
11-
$ENV{DANCER_CONFIG_READERS}
12-
= 'Dancer2::ConfigReader::File::Simple,Dancer2::ConfigReader::TestDummy';
11+
$ENV{DANCER_CONFDIR} = './t/app/t1';
1312
}
1413
use lib '.';
1514
use lib './t/lib';
1615

17-
use t::app::t1::lib::App1;
16+
use Dancer2::Core::App;
17+
18+
subtest basic => sub {
19+
$ENV{DANCER_CONFIG_READERS}
20+
= 'Dancer2::ConfigReader::File::Simple,Dancer2::ConfigReader::TestDummy';
21+
my $app = Dancer2::Core::App->new( name => 'basic' );
22+
23+
is $app->config->{app}->{config}, 'ok',
24+
$app->name . ": config loaded properly";
25+
is $app->config->{dummy}->{dummy_subitem}, 2,
26+
$app->name . ": dummy config loaded properly";
27+
};
28+
29+
subtest additional_config_readers => sub {
30+
$ENV{DANCER_CONFIG_READERS} = 'Dancer2::ConfigReader::Additional';
1831

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

21-
is $app->config->{app}->{config}, 'ok',
22-
$app->name . ": config loaded properly";
23-
is $app->config->{dummy}->{dummy_subitem}, 2,
24-
$app->name . ": dummy config loaded properly";
34+
is $app->config->{app}->{config}, 'ok',
35+
$app->name . ": config loaded properly";
36+
is $app->config->{dummy}->{dummy_subitem}, 2,
37+
$app->name . ": dummy config loaded properly";
38+
};
2539

2640
done_testing;

t/config_many_failure.t

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,39 @@ use strict;
22
use warnings;
33

44
use Test::More;
5+
use Test::Exception;
56
use File::Spec;
67
use English;
78

9+
use Dancer2::Core::App;
10+
811
BEGIN {
912
# undefine ENV vars used as defaults for app environment in these tests
1013
local $ENV{DANCER_ENVIRONMENT};
1114
local $ENV{PLACK_ENV};
12-
$ENV{DANCER_CONFIG_READERS}
13-
= 'Dancer2::ConfigReader::File::Simple Dancer2::ConfigReader::TestDummy';
1415
}
1516

1617
use lib q{.};
1718
use lib './t/lib';
1819

19-
local $EVAL_ERROR = undef;
20-
my $eval_r = eval 'use t::app::t1::lib::App1;';
21-
my $eval_e = $EVAL_ERROR;
22-
is $eval_r, undef, 'Eval failed correctly';
23-
like $eval_e, qr{`Dancer2::ConfigReader::File::Simple Dancer2::ConfigReader::TestDummy' is not a module name}, 'Correct dying and error';
20+
subtest 'bad DANCER_CONFIG_READERS' => sub {
21+
# space instead of comma, ooops
22+
$ENV{DANCER_CONFIG_READERS}
23+
= 'Dancer2::ConfigReader::File::Simple Dancer2::ConfigReader::TestDummy';
24+
25+
throws_ok {
26+
Dancer2::Core::App->new( name => 'basic' );
27+
} qr{`Dancer2::ConfigReader::File::Simple Dancer2::ConfigReader::TestDummy' is not a module name};
28+
};
29+
30+
subtest 'infinite loop of configs' => sub {
31+
$ENV{DANCER_CONFIG_READERS}
32+
= 'Dancer2::ConfigReader::Recursive';
33+
34+
throws_ok {
35+
Dancer2::Core::App->new( name => 'basic' );
36+
} qr{MAX_CONFIGS exceeded};
37+
38+
};
2439

2540
done_testing;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package Dancer2::ConfigReader::Additional;
2+
use Moo;
3+
use Dancer2::Core::Factory;
4+
use Dancer2::Core;
5+
use Dancer2::Core::Types;
6+
use Dancer2::FileUtils 'path';
7+
8+
with 'Dancer2::Core::Role::ConfigReader';
9+
10+
has name => (
11+
is => 'ro',
12+
isa => Str,
13+
lazy => 0,
14+
default => 'Additional',
15+
);
16+
17+
has config_files => (
18+
is => 'ro',
19+
lazy => 1,
20+
isa => ArrayRef,
21+
default => sub {
22+
my ($self) = @_;
23+
return [];
24+
},
25+
);
26+
27+
sub read_config {
28+
return {
29+
additional_config_readers => [qw/
30+
Dancer2::ConfigReader::File::Simple
31+
Dancer2::ConfigReader::TestDummy
32+
/]
33+
};
34+
}
35+
36+
1;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package Dancer2::ConfigReader::Recursive;
2+
use Moo;
3+
use Dancer2::Core::Factory;
4+
use Dancer2::Core;
5+
use Dancer2::Core::Types;
6+
use Dancer2::FileUtils 'path';
7+
8+
with 'Dancer2::Core::Role::ConfigReader';
9+
10+
has name => (
11+
is => 'ro',
12+
isa => Str,
13+
lazy => 0,
14+
default => 'Recursive',
15+
);
16+
17+
has config_files => (
18+
is => 'ro',
19+
lazy => 1,
20+
isa => ArrayRef,
21+
default => sub {
22+
my ($self) = @_;
23+
return [];
24+
},
25+
);
26+
27+
sub read_config {
28+
return {
29+
additional_config_readers =>
30+
'Dancer2::ConfigReader::Recursive'
31+
};
32+
}
33+
34+
1;

0 commit comments

Comments
 (0)