File Coverage

blib/lib/Kelp/Module/Config.pm
Criterion Covered Total %
statement 75 81 92.5
branch 23 28 82.1
condition n/a
subroutine 19 21 90.4
pod 4 5 80.0
total 121 135 89.6


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