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