File Coverage

blib/lib/Kelp/Module/Config.pm
Criterion Covered Total %
statement 89 96 92.7
branch 37 42 88.1
condition 8 9 88.8
subroutine 18 20 90.0
pod 4 4 100.0
total 156 171 91.2


line stmt bran cond sub pod time code
1              
2             use Kelp::Base 'Kelp::Module';
3 37     37   202573 use Carp;
  37         87  
  37         265  
4 37     37   1920 use Try::Tiny;
  37         66  
  37         1491  
5 35     35   177 use Test::Deep;
  35         255  
  35         1338  
6 35     35   6988 use Path::Tiny;
  35         259707  
  35         233  
7 35     35   33000  
  35         345289  
  35         46058  
8              
9             # Extension to look for
10             attr ext => 'pl';
11              
12             # Directory where config files are
13             attr path => sub {
14             my $self = shift;
15             return [
16             $ENV{KELP_CONFIG_DIR},
17             'conf',
18             $self->app->path,
19             $self->app->path . '/conf',
20             $self->app->path . '/../conf'
21             ]
22             };
23              
24             attr separator => sub { qr/\./ };
25              
26             # Defaults
27             attr data => sub {
28             my $self = shift;
29              
30             # Return a big hash with default values
31             return {
32              
33             # Default charset is UTF-8
34             charset => 'UTF-8',
35              
36             app_url => 'http://localhost:5000',
37              
38             # Modules to load
39             modules => [qw/JSON Template/],
40              
41             # Module initialization params
42             modules_init => {
43              
44             # Routes
45             Routes => {
46             base => ref( $self->app )
47             },
48              
49             # Template
50             Template => {
51             paths => [
52             $self->app->path . '/views',
53             $self->app->path . '/../views'
54             ]
55             },
56              
57             # JSON
58             JSON => {
59             allow_blessed => 1,
60             convert_blessed => 1,
61             utf8 => 1
62             },
63             },
64              
65             # List of the middleware to add
66             middleware => [],
67              
68             # Initializations of the middleware
69             middleware_init => {},
70              
71             };
72             };
73              
74             my ( $self, $path ) = @_;
75             return unless $path;
76 485     485 1 1654 my @a = split( $self->separator, $path );
77 485 100       936 my $val = $self->data;
78 483         1002 for my $chunk (@a) {
79 483         1124 if ( ref($val) eq 'HASH' ) {
80 483         865 $val = $val->{$chunk};
81 632 100       1241 }
82 631         1223 else {
83             croak "Config path $path breaks at '$chunk'";
84             }
85 1         23 }
86             return $val;
87             }
88 482         1797  
89             # Override this one to use other config formats.
90             my ( $self, $filename ) = @_;
91              
92             # Open and read file
93 34     34 1 74 my $text;
94             try {
95             $text = path($filename)->slurp_utf8;
96 34         49 };
97              
98 34     34   703 if (!defined $text) {
99 34         208 warn "Can not read config file " . $filename;
100             return {};
101 34 50       25680 }
102 0         0  
103 0         0 my ( $hash, $error );
104             {
105             local $@;
106 34         73 my $app = $self->app;
107             my $module = $filename;
108 34         47 $module =~ s/\W/_/g;
  34         58  
109 34         111 $hash =
110 34         66 eval "package Kelp::Module::Config::Sandbox::$module;"
111 34         244 . "use Kelp::Base -strict;"
112 20     20   138 . "sub app; local *app = sub { \$app };"
  20     5   56  
  20         138  
  5         30  
  5         8  
  5         26  
  34         2936  
113             . "sub include(\$); local *include = sub { \$self->load(\@_) };"
114             . $text;
115             $error = $@;
116             }
117              
118 34         475 die "Config file $filename parse error: " . $error if $error;
119             die "Config file $filename did not return a HASH - $hash"
120             unless ref $hash eq 'HASH';
121 34 100       109  
122 33 100       114 return $hash;
123             }
124              
125 32         100 my ( $self, $mode ) = @_;
126              
127             my $filename = sub {
128             my @paths = ref( $self->path ) ? @{ $self->path } : ( $self->path );
129 97     97 1 185 for my $path (@paths) {
130             next unless defined $path;
131             my $filename = sprintf( '%s/%s.%s', $path, $mode, $self->ext );
132 97 100   97   225 return $filename if -r $filename;
  85         201  
133 97         196 }
134 350 100       749 }->();
135 280         604  
136 280 100       3890 unless ( $filename ) {
137             if ( $ENV{KELP_CONFIG_WARN} ) {
138 97         383 my $message =
139             $mode eq 'config'
140 97 100       597 ? "Main config file not found or not readable"
141 62 50       336 : "Config file for mode '$mode' not found or not readable";
142 0 0       0 warn $message;
143             }
144             return;
145             }
146 0         0  
147             my $parsed = {};
148 62         192 try {
149             $parsed = $self->load($filename);
150             }
151 35         76 catch {
152             die "Parsing $filename died with error: '${_}'";
153 35     35   1737 };
154             $self->data( _merge( $self->data, $parsed ) );
155             }
156 2     2   51  
157 35         289 my ( $self, %args ) = @_;
158 33         551  
159             # Find, parse and merge 'config' and mode files
160             for my $name ( 'config', $self->app->mode ) {
161             $self->process_mode( $name );
162 48     48 1 175 }
163              
164             # Undocumented! Add 'extra' argument to unlock these special features:
165 48         117 # 1. If the extra argument contains a HASH, it will be merged to the
166 94         315 # configuration upon loading.
167             # 2. A new attribute '_cfg' will be registered into the app, which has
168             # three methods: merge, clear and set. Use them to merge a hash into
169             # the configuration, clear it, or set it to a new value. You can do those
170             # at any point in the life of the app.
171             #
172             if ( my $extra = delete $args{extra} ) {
173             $self->data( _merge( $self->data, $extra ) ) if ref($extra) eq 'HASH';
174             $self->register(
175              
176             # A tiny object containing only merge, clear and set. Very useful when
177 46 100       155 # you're writing tests and need to add new config options, set the
178 6 100       37 # entire config hash to a new value, or clear it completely.
179             _cfg => Plack::Util::inline_object(
180             merge => sub {
181             $self->data( _merge( $self->data, $_[0] ) );
182             },
183             clear => sub { $self->data( {} ) },
184             set => sub { $self->data( $_[0] ) }
185             )
186 1     1   25 );
187             }
188 0     0   0  
189 0     0   0 $self->register(
190              
191 6         82 # Return the entire config hash
192             config_hash => $self->data,
193              
194             # A wrapper arount the get method
195             config => sub {
196             my ( $app, $path ) = @_;
197             return $self->get($path);
198             }
199             );
200             }
201 476     476   927  
202 476         1171 my ( $a, $b, $sigil ) = @_;
203              
204 46         168 return $b
205             if !ref($a)
206             || !ref($b)
207             || ref($a) ne ref($b);
208 139     139   17553  
209             if ( ref $a eq 'ARRAY' ) {
210 139 100 66     832 return $b unless $sigil;
      100        
211             if ( $sigil eq '+' ) {
212             for my $e (@$b) {
213             push @$a, $e unless grep { eq_deeply( $_, $e ) } @$a;
214             }
215 125 100       362 }
    50          
216 41 100       122 else {
217 30 100       96 $a = [
218 5         11 grep {
219 11 100       641 my $e = $_;
  25         12291  
220             !grep { eq_deeply( $_, $e ) } @$b
221             } @$a
222             ];
223             }
224             return $a;
225 25         64 }
  51         34273  
226 51         91 elsif ( ref $a eq 'HASH' ) {
  64         2451  
227             for my $k ( keys %$b ) {
228              
229             # If the key is an array then look for a merge sigil
230 30         3605 my $s = ref($b->{$k}) eq 'ARRAY' && $k =~ s/^(\+|\-)// ? $1 : '';
231              
232             $a->{$k} =
233 84         227 exists $a->{$k}
234             ? _merge( $a->{$k}, $b->{"$s$k"}, $s )
235             : $b->{$k};
236 93 100 100     558 }
237              
238             return $a;
239             }
240             return $b;
241 93 100       427 }
242              
243             1;
244 84         244  
245              
246 0           =pod
247              
248             =head1 NAME
249              
250             Kelp::Module::Config - Configuration for Kelp applications
251              
252             =head1 DESCRIPTION
253              
254             This is one of the two modules that are automatically loaded for each and every
255             Kelp application. The other one is L<Kelp::Module::Routes>. It reads
256             configuration files containing Perl-style hashes, and merges them depending on
257             the value of the application's C<mode> attribute.
258              
259             The main configuration file name is C<config.pl>, and it will be searched in
260             the C<conf> and C<../conf> directories. You can also set the C<KELP_CONFIG_DIR>
261             environmental variable with the path to the configuration files.
262              
263             This module comes with some L<default values|/DEFAULTS>, so if there are no
264             configuration files found, those values will be used. Any values from
265             configuration files will add to or override the default values.
266              
267             =head1 ORDER
268              
269             First the module will look for C<conf/config.pl>, then for
270             C<../conf/config.pl>. If found, they will be parsed and merged into the
271             default values. The same order applies to the I<mode> file too, so if the
272             application L<mode|Kelp/mode> is I<development>, then C<conf/development.pl>
273             and C<../conf/development.pl> will be looked for. If found, they will also be
274             merged to the config hash.
275              
276             =head1 ACCESSING THE APPLICATION
277              
278             The application instance can be accessed within the config files via the C<app>
279             keyword.
280              
281             {
282             bin_path => app->path . '/bin'
283             }
284              
285             =head1 INCLUDING FILES
286              
287             To include other config files, one may use the C<include> keyword.
288              
289             # config.pl
290             {
291             modules_init => {
292             Template => include('conf/my_template.pl')
293             }
294             }
295              
296             # my_template.pl
297             {
298             path => 'views/',
299             utf8 => 1
300             }
301              
302             Any config file may be included as long as it returns a hashref.
303              
304             =head1 MERGING
305              
306             The first configuration file this module will look for is C<config.pl>. This is
307             where you should keep configuration options that apply to all running
308             environments. The mode-specific configuration file will be merged to this
309             config, and it will take priority. Merging is done as follows:
310              
311             =over
312              
313             =item Scalars will always be overwritten.
314              
315             =item Hashes will be merged.
316              
317             =item Arrays will be overwritten, except in case when the name of the array contains a
318             sigil as follows:
319              
320             =over
321              
322             =item
323              
324             C<+> in front of the name will add the elements to the array:
325              
326             # in config.pl
327             {
328             middleware => [qw/Bar Foo/]
329             }
330              
331             # in development.pl
332             {
333             '+middleware' => ['Baz'] # Add 'Baz' in development
334             }
335              
336             =item
337              
338             C<-> in front of the name will remove the elements from the array:
339              
340             # in config.pl
341             {
342             modules => [qw/Template JSON Logger/]
343             }
344              
345             # in test.pl
346             {
347             '-modules' => [qw/Logger/] # Remove the Logger modules in test mode
348             }
349              
350             =item
351              
352             No sigil will cause the array to be completely replaced:
353              
354             # in config.pl
355             {
356             middleware => [qw/Bar Foo/]
357             }
358              
359             # in cli.pl
360             {
361             middleware => [] # No middleware in CLI
362             }
363              
364             =back
365              
366             Note that the merge sigils only apply to arrays. All other types will keep the
367             sigil in the key name:
368              
369             # config.pl
370             {
371             modules => ["+MyApp::Fully::Qualified::Name"],
372             modules_init => {
373             "+MyApp::Fully::Qualified::Name" => { opt1 => 1, opt2 => 2 }
374             }
375             }
376              
377             # development.pl
378             {
379             modules_init => {
380             "+MyApp::Fully::Qualified::Name" => { opt3 => 3 }
381             }
382             }
383              
384             =back
385              
386             =head1 REGISTERED METHODS
387              
388             This module registers the following methods into the underlying app:
389              
390             =head2 config
391              
392             A wrapper for the C</get> method.
393              
394             # Somewhere in the app
395             my $pos = $self->config('row.col.position');
396              
397             # Gets {row}->{col}->{position} from the config hash
398              
399             =head2 config_hash
400              
401             A reference to the entire configuration hash.
402              
403             my $pos = $self->config_hash->{row}->{col}->{position};
404              
405             Using this or C<config> is entirely up to the application developer.
406              
407             =head3 _cfg
408              
409             A tiny object that contains only three methods - B<merge>, B<clear> and B<set>.
410             It allows you to merge values to the config hash, clear it completely or
411             set it to an entirely new value. This method comes handy when writing tests.
412              
413             # Somewhere in a .t file
414             my $app = MyApp->new( mode => 'test' );
415              
416             my %original_config = %{ $app->config_hash };
417             $app->_cfg->merge( { middleware => ['Foo'] } );
418              
419             # Now you can test with middleware Foo added to the config
420              
421             # Revert to the original configuration
422             $app->_cfg->set( \%original_config );
423              
424             =head1 ATTRIBUTES
425              
426             This module implements some attributes, which can be overridden by subclasses.
427              
428             =head2 ext
429              
430             The file extension of the configuration files. Default is C<pl>.
431              
432             =head2 separator
433              
434             A regular expression for the value separator used by L</get>. The default is
435             C<qr/\./>, i.e. a dot.
436              
437             =head2 path
438              
439             Specifies a path, or an array of paths where to look for configuration files.
440             This is particularly useful when writing tests, because you can set a custom
441             path to a peculiar configuration.
442              
443             =head2 data
444              
445             The hashref with data contained in all of the merged configurations.
446              
447             =head1 METHODS
448              
449             The module also implements some methods for parsing the config files, which can
450             be overridden in extending classes.
451              
452             =head2 get
453              
454             C<get($string)>
455              
456             Get a value from the config using a separated string.
457              
458             my $value = $c->get('bar.foo.baz');
459             my $same = $c->get('bar')->{foo}->{baz};
460             my $again = $c->data->{bar}->{foo}->{baz};
461              
462             By default the separator is a dot, but this can be changed via the
463             L</separator> attribute.
464              
465             =head2 load
466              
467             C<load(filename)>
468              
469             Loads, and parses the file C<$filename> and returns a hash reference.
470              
471             =head2 process_mode
472              
473             C<process_mode($mode)>
474              
475             Finds the file (if it exists) corresponding to C<$mode>, parses it and merges
476             it into the data. Useful, when you want to process and extra config file during
477             the application initialization.
478              
479             # lib/MyApp.pm
480             sub build {
481             $self->loaded_modules->{Config}->process_mode( 'more_config' );
482             }
483              
484             =head1 DEFAULTS
485              
486             This module sets certain default values. All of them may be overridden in any of
487             the C<conf/> files. It probably pays to view the code of this module and look
488             and the C<defaults> sub to see what is being set by default, but here is the
489             short version:
490              
491             =head2 charset
492              
493             C<UTF-8>
494              
495             =head2 app_url
496              
497             C<http://localhost:5000>
498              
499             =head2 modules
500              
501             An arrayref with module names to load on startup. The default value is
502             C<['JSON', 'Template']>
503              
504             =head2 modules_init
505              
506             A hashref with initializations for each of the loaded modules, except this one,
507             ironically.
508              
509             =head2 middleware
510              
511             An arrayref with middleware to load on startup. The default value is an
512             empty array.
513              
514             =head2 middleware_init
515              
516             A hashref with initialization arguments for each of the loaded middleware.
517              
518             =head1 SUBCLASSING
519              
520             You can subclass this module and use other types of configuration files
521             (for example YAML). You need to override the C<ext> attribute
522             and the C<load> subroutine.
523              
524             package Kelp::Module::Config::Custom;
525             use Kelp::Parent 'Kelp::Module::Config';
526              
527             # Set the config file extension to .cus
528             attr ext => 'cus';
529              
530             sub load {
531             my ( $self, $filename ) = @_;
532              
533             # Load $filename, parse it and return a hashref
534             }
535              
536             1;
537              
538             Later ...
539              
540             # app.psgi
541             use MyApp;
542              
543             my $app = MyApp->new( config_module => 'Config::Custom' );
544              
545             run;
546              
547             The above example module will look for C<config/*.cus> to load as configuration.
548              
549             =head1 TESTING
550              
551             Since the config files are searched in both C<conf/> and C<../conf/>, you can
552             use the same configuration set of files for your application and for your tests.
553             Assuming the all of your test will reside in C<t/>, they should be able to load
554             and find the config files at C<../conf/>.
555              
556             =head1 ENVIRONMENT VARIABLES
557              
558             =head2 KELP_CONFIG_WARN
559              
560             This module will not warn for missing config and mode files. It will
561             silently load the default configuration hash. Set KELP_CONFIG_WARN to a
562             true value to make this module warn about missing files.
563              
564             $ KELP_CONFIG_WARN=1 plackup app.psgi
565              
566             =cut