diff --git a/README.md b/README.md index 6a27b1a01..0b3e25ae7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cpanfile b/cpanfile index 7e67f9319..6049fcaba 100644 --- a/cpanfile +++ b/cpanfile @@ -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'; diff --git a/lib/Dancer2/Config.pod b/lib/Dancer2/Config.pod index 48ca1a03a..01f89ab32 100644 --- a/lib/Dancer2/Config.pod +++ b/lib/Dancer2/Config.pod @@ -185,6 +185,11 @@ PSGI middleware. The default middleware wrappers are: =back +=head3 additional_config_readers + +L instances to bootstrap the application's +configuration. See the ConfigReader's POD for more information. + =head2 Content type / character set =head3 content_type (string) @@ -597,6 +602,11 @@ Sets the configuration directory. This correlates to the C config option. +=head2 DANCER_CONFIG_VERBOSE + +Outputs a lot of debugging information when generating the configuration of +the application. + =head3 DANCER_ENVDIR Sets the environment directory. diff --git a/lib/Dancer2/ConfigReader.pm b/lib/Dancer2/ConfigReader.pm index 9ba79f88c..0ea7c3ea0 100644 --- a/lib/Dancer2/ConfigReader.pm +++ b/lib/Dancer2/ConfigReader.pm @@ -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, @@ -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; + + 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 { @@ -116,26 +162,18 @@ __END__ =head1 DESCRIPTION -This class provides a C attribute that - when accessing -the first time - feeds itself by executing one or more -B packages. +This class provides a C attribute that +is populated by executing one or more B packages. +The default ConfigReader used by default is C. Also provides a C method which is supposed to be used by externals to read/write config entries. -You can control which B -class or classes to use to create the config. - -Use C 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 @@ -149,7 +187,7 @@ For example, if config subitem1: subcontent1 subitem2: subcontent2 -was followed by config +and the second returns item2: content9 item3: @@ -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 @@ -175,19 +213,42 @@ then the final config would be subsubitem5: subsubcontent5 item4: content4 -The default B is C. +=head2 Configuring the ConfigReaders via DANCER_CONFIG_READERS + +You can control which B +class or classes to use to create the config +via the C 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 + +If the key C 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 that is + + additional_config_readers: + - Dancer2::ConfigReader::SQLite: + path: /path/to/sqlite.db + table: config + +The default ConfigReader L will pick that file and proceed to instantiate C +with the provided parameters. + +C 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 classes. +=head2 Creating your own custom B classes. -If you want, you can also extend class C. -Here is an example: +Here's an example extending class C. - 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) = @_; diff --git a/t/config_many.t b/t/config_many.t index 9f4e9f37e..39b597ab2 100644 --- a/t/config_many.t +++ b/t/config_many.t @@ -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; diff --git a/t/config_many_failure.t b/t/config_many_failure.t index 54b32f656..8c4fc95d2 100644 --- a/t/config_many_failure.t +++ b/t/config_many_failure.t @@ -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; diff --git a/t/lib/Dancer2/ConfigReader/Additional.pm b/t/lib/Dancer2/ConfigReader/Additional.pm new file mode 100644 index 000000000..18745dfb4 --- /dev/null +++ b/t/lib/Dancer2/ConfigReader/Additional.pm @@ -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; diff --git a/t/lib/Dancer2/ConfigReader/Recursive.pm b/t/lib/Dancer2/ConfigReader/Recursive.pm new file mode 100644 index 000000000..ac4b5558f --- /dev/null +++ b/t/lib/Dancer2/ConfigReader/Recursive.pm @@ -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;