File Coverage

blib/lib/PFT/Conf.pm
Criterion Covered Total %
statement 126 145 86.9
branch 28 42 66.6
condition 11 17 64.7
subroutine 25 29 86.2
pod 8 10 80.0
total 198 243 81.4


line stmt bran cond sub pod time code
1             # Copyright 2014-2016 - Giovanni Simoni
2             #
3             # This file is part of PFT.
4             #
5             # PFT is free software: you can redistribute it and/or modify it under the
6             # terms of the GNU General Public License as published by the Free
7             # Software Foundation, either version 3 of the License, or (at your
8             # option) any later version.
9             #
10             # PFT is distributed in the hope that it will be useful, but WITHOUT ANY
11             # WARRANTY; without even the implied warranty of MERCHANTABILITY or
12             # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13             # for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with PFT. If not, see .
17             #
18             package PFT::Conf v1.3.0;
19              
20             =encoding utf8
21              
22             =head1 NAME
23              
24             PFT::Conf - Configuration parser for PFT
25              
26             =head1 SYNOPSIS
27              
28             PFT::Conf->new_default() # Using default
29             PFT::Conf->new_load($root) # Load from conf file in directory
30             PFT::Conf->new_load_locate() # Load from conf file, find directory
31             PFT::Conf->new_load_locate($cwd)
32              
33             PFT::Conf::locate() # Locate root
34             PFT::Conf::locate($cwd)
35              
36             PFT::Conf::isroot($path) # Check if location exists under path.
37              
38             use Getopt::Long;
39             Getopt::Long::Configure 'bundling';
40             GetOptions(
41             PFT::Conf::wire_getopt(\my %opts),
42             'more-opt' => \$more,
43             );
44             PFT::Conf->new_getopt(\%opts); # Create with command line options
45              
46             =head1 DESCRIPTION
47              
48             Automatic loader and handler for the configuration file of a I site.
49              
50             The configuration is a simple I file with a conventional name. Some
51             keys are mandatory, while other are optional. This module allows a
52             headache free check for mandatory ones.
53              
54             =head2
55              
56             Many constructors are available, here described:
57              
58             =over
59              
60             =item new_default
61              
62             Creates a new configuration based on environment variables and common
63             sense.
64              
65             The configuration can later be stored on a file with the C
66             method.
67              
68             =item new_load
69              
70             Loads a configuration file which must already exist. Accepts as optional
71             argument the name of a directory (not encoded), which defaults on
72             the current directory.
73              
74             This constructor fails with C if the directory does not contain a
75             configuration file.
76              
77             =item new_load_locate
78              
79             Works as C, but before failing makes an attempt to locate the
80             configuration file in the parent directories up to the root level.
81              
82             This is handy for launching commands from the command line without
83             worrying on the current directory: it works as long as your I is
84             below a I root directory.
85              
86             =item wire_getopt and new_getopt
87              
88             This is a two-steps constructor meant for command line initializers.
89              
90             An example of usage can be found in the B section. In short, the
91             auxiliary function C provides a list of
92             ready-to-use options for the C Perl module. It expects a
93             hash reference as argument, which will be used as storage for selected
94             options. The C constructor expects as argument the same hash
95             reference.
96              
97             =back
98              
99             =cut
100              
101 3     3   74328 use utf8;
  3         16  
  3         18  
102 3     3   108 use v5.16;
  3         10  
103 3     3   18 use strict;
  3         4  
  3         82  
104 3     3   16 use warnings;
  3         6  
  3         89  
105              
106 3     3   13 use Carp;
  3         6  
  3         179  
107 3     3   17 use Cwd;
  3         6  
  3         165  
108 3     3   518 use Encode::Locale;
  3         15946  
  3         140  
109 3     3   21 use Encode;
  3         6  
  3         283  
110 3     3   21 use File::Basename qw/dirname/;
  3         5  
  3         237  
111 3     3   20 use File::Path qw/make_path/;
  3         7  
  3         162  
112 3     3   1493 use File::Spec::Functions qw/updir catfile catdir rootdir/;
  3         2628  
  3         219  
113 3     3   668 use YAML::Tiny;
  3         6129  
  3         182  
114              
115             =head2 Shared variables
116              
117             C<$PFT::Conf::CONF_NAME> is a string. Defines the name of the
118             configuration file.
119              
120             =cut
121              
122 3     3   23 use Exporter 'import';
  3         6  
  3         5786  
123             our @EXPORT_OK = qw(
124             pod_autogen
125             bash_completion_autogen
126             );
127             our $CONF_NAME = 'pft.yaml';
128              
129             # %CONF_RECIPE maps configuration names to an array.
130             #
131             # Keys of this map correspond to keys in the configuratoin file. They use dashes
132             # to identify the hierarchy, so that, for instance, site-author corresponds to
133             # the key 'author' in the section 'site' of the configuration file.
134             #
135             # Keys are also used for generating automatically the POD user guide and the
136             # command line options of the `pft init` command.
137             #
138             # The semantics of each array item is defined by the following $IDX_* variables:
139             my(
140             $IDX_MANDATORY, # true if the configuration is mandatory
141             $IDX_GETOPT_SUFFIX, # the corresponding suffix in getopt (see Getopt::Long)
142             $IDX_DEFAULT, # The default value when generating a configuration
143             $IDX_HELP # A human readable text descrbing the option
144             ) = 0 .. 3;
145             my %CONF_RECIPE = do {
146             my $user = $ENV{USER} || 'anon';
147             my $editor = $ENV{EDITOR} || 'vim';
148             my $browser = $ENV{BROWSER} || 'firefox';
149             (
150             'site-author' => [1, '=s', $user || 'Anonymous',
151             'Default author of entries'
152             ],
153             'site-template' => [1, '=s', 'default.html',
154             'Default template for compilation, can be overriden by single '.
155             'entries'
156             ],
157             'site-theme' => [0, '=s', 'light',
158             'Global theme (e.g. "light" or "dark") optionally honored by '.
159             'templates. Specific accepted values depend on the template '.
160             'implementation'
161             ],
162             'site-title' => [1, '=s', 'My PFT website',
163             'Title of the website',
164             ],
165             'site-url' => [0, '=s', 'http://example.org',
166             'Base url for the website',
167             ],
168             'site-home' => [1, '=s', 'Welcome',
169             'First page, where index.html redirects the browsers',
170             ],
171             'site-encoding' => [1, '=s', $Encode::Locale::ENCODING_LOCALE,
172             'Charset of the generated web pages'
173             ],
174             'site-feedfile' => [0, '=s', 'feed.rss',
175             'File name of the RSS XML to be published by "pft gen-rss"',
176             ],
177             'publish-method' => [1, '=s', 'rsync+ssh',
178             'Method used for publishing'
179             ],
180             'publish-host' => [0, '=s', 'example.org',
181             'Remote host where to publish'
182             ],
183             'publish-user' => [0, '=s', $user,
184             'User login on publishing host'
185             ],
186             'publish-port' => [0, '=i', 22,
187             'Port for connection on publishing host'
188             ],
189             'publish-path' => [0, '=s', "/home/$user/public_html",
190             'Directory on publishing host'
191             ],
192             'system-editor' => [0, '=s', "$editor %s",
193             'Editor to be invoked by C. You may specify an '.
194             'executable, or a shell command where "%s" gets replaced '.
195             'with the file name'
196             ],
197             'system-browser' => [0, '=s', "$browser %s",
198             'Browser to be invoked by C. You may specify an '.
199             'executable, or a shell command where "%s" gets replaced '.
200             'with the file name'
201             ],
202             )
203             };
204              
205             # Transforms a flat mapping as $CONF_RECIPE into 'deep' hash table. Items in the
206             # form 'foo-bar-baz' will be accessible as _hashify()->{foo}{bar}{baz}.
207             sub _hashify {
208 7     7   125 my %out;
209              
210 7 50       42 @_ % 2 and die "Odd number of args";
211 7         29 for (my $i = 0; $i < @_; $i += 2) {
212 67 50       140 defined(my $val = $_[$i + 1]) or next;
213 67         139 my @keys = split /-/, $_[$i];
214              
215 67 50       114 die "Key is empty? \"$_[$i]\"" unless @keys;
216 67         96 my $dst = \%out;
217 67         113 while (@keys > 1) {
218 69         98 my $k = shift @keys;
219             $dst = exists $dst->{$k}
220             ? $dst->{$k}
221 69 100       131 : do { $dst->{$k} = {} };
  17         55  
222 69 100       411 ref $dst ne 'HASH' and croak "Not pointing to hash: $_[$i]";
223             }
224 66         100 my $k = shift @keys;
225 66 100 66     238 exists $dst->{$k} && ref $dst->{$k} eq 'HASH'
226             and croak "Overwriting $_[$i]";
227 65         181 $dst->{$k} = $val;
228             }
229              
230 5         20 \%out;
231             }
232              
233             # Read the %CONF_RECIPE map and return a mapping between each key and the
234             # associated field. The first parameter is the index to select. The second
235             # parameter is optional: if true retrieves only the configuration which evaluate
236             # as true.
237             sub _read_recipe {
238 7     7   19 my $select = shift;
239 7         14 my @out;
240 7 100       26 if (my $filter = shift) {
241 3         19 while (my($k, $vs) = each %CONF_RECIPE) {
242 45 100       116 my $v = $vs->[$select] or next;
243 18         54 push @out, $k => $vs->[$select];
244             }
245             } else {
246 4         33 while (my($k, $vs) = each %CONF_RECIPE) {
247 60         194 push @out, $k => $vs->[$select];
248             }
249             }
250 7         46 @out;
251             }
252              
253             sub pod_autogen {
254 0     0 0 0 my @out = ('=over', '');
255              
256 0         0 for my $key (sort keys %CONF_RECIPE) {
257 0         0 my $info = $CONF_RECIPE{$key};
258 0         0 push @out,
259             "=item --$key", '',
260             $info->[$IDX_HELP], '',
261             "Defaults to C<$info->[$IDX_DEFAULT]>", ''
262             }
263              
264 0         0 join "\n", @out, '=back';# '', '=cut';
265             }
266              
267             sub bash_completion_autogen {
268 0     0 0 0 '--' . join "\n--", keys %CONF_RECIPE;
269             }
270              
271             sub new_default {
272 4     4 1 26 my $self = _hashify(_read_recipe($IDX_DEFAULT));
273 4         15 $self->{_root} = undef;
274 4         16 bless $self, shift;
275             }
276              
277             sub _check_assign {
278 3     3   56 my $self = shift;
279 3         9 local $" = '-';
280 3         8 my $i;
281              
282 3         12 for my $mandk (grep { ++$i % 2 } _read_recipe($IDX_MANDATORY, 1)) {
  36         56  
283 18         44 my @keys = split /-/, $mandk;
284 18         23 my @path;
285              
286 18         24 my $c = $self;
287 18         32 while (@keys > 1) {
288 18         35 push @path, (my $k = shift @keys);
289 18 50       40 confess "Missing section \"@path\"" unless $c->{$k};
290 18         26 $c = $c->{$k};
291 18 50       65 confess "Seeking \"@keys\" in \"@path\""
292             unless ref $c eq 'HASH';
293             }
294 18         28 push @path, shift @keys;
295 18 100       231 confess "Missing @path" unless exists $c->{$path[-1]};
296             }
297             }
298              
299             sub new_load {
300 2     2 1 9 my($cls, $root) = @_;
301              
302 2         4 my $self = do {
303 2 50       6 my $enc_fname = isroot($root)
304             or croak "$root is not a PFT site: $CONF_NAME is missing";
305 2 50       120 open(my $f, '<:encoding(locale)', $enc_fname)
306             or croak "Cannot open $CONF_NAME in $root $!";
307 2         133 local $/ = undef;
308 2         117 my $yaml = <$f>;
309 2         56 close $f;
310              
311 2         14 YAML::Tiny::Load($yaml);
312             };
313 2         2387 _check_assign($self);
314              
315 2         6 $self->{_root} = $root;
316 2         23 bless $self, $cls;
317             }
318              
319             sub new_load_locate {
320 1     1 1 5 my $cls = shift;
321 1         5 my $root = locate(my $start = shift);
322 1 50       6 croak "Not a PFT site (or any parent up to $start)"
323             unless defined $root;
324              
325 1         5 $cls->new_load($root);
326             }
327              
328             sub new_getopt {
329 0     0 1 0 my($cls, $wired_hash) = @_;
330              
331 0         0 my $self = _hashify(
332             _read_recipe($IDX_DEFAULT), # defaults
333             %$wired_hash, # override via wire_getopt
334             );
335 0         0 $self->{_root} = undef;
336 0         0 bless $self, $cls;
337             }
338              
339             =head2 Utility functions
340              
341             =over
342              
343             =item isroot
344              
345             The C function searches for the configuration file in
346             the given directory path (not encoded).
347              
348             Returns C if the file was not found, and the encoded file name
349             (according to locale) if it was found.
350              
351             =cut
352              
353             sub isroot {
354 24     24 1 156 my $f = encode(locale_fs => catfile(shift, $CONF_NAME));
355 24 100       1532 -e $f ? $f : undef
356             }
357              
358             =item locate
359              
360             The C function locates a I configuration file.
361              
362             It accepts as optional parameter a directory path (not encoded),
363             defaulting on the current working directory.
364              
365             Possible return values:
366              
367             =over
368              
369             =item The input directory itself if the configuration file was
370             found in it;
371              
372             =item The first encountered parent directory containing the configuration
373             file;
374              
375             =item C if no configuration file was found, up to the root of all
376             directories.
377              
378             =back
379              
380             =back
381              
382             =cut
383              
384             sub locate {
385 9   66 9 1 8441 my $cur = shift || Cwd::getcwd;
386 9         29 my $root;
387              
388 9 50       45 croak "Not a directory: $cur" unless -d encode(locale_fs => $cur);
389              
390             # No single root directory on Windows. File::Spec->rootdir does not
391             # work as intended. Workaround: $prev is like $cur on the previous
392             # step: we stay on the same directory even going up, we reached the
393             # root. Thanks to Alexandr Ciornii for checking this.
394 9         1016 my $prev = '';
395 9   100     91 until ($cur eq rootdir or $cur eq $prev or defined($root)) {
      66        
396 20         103 $prev = $cur;
397 20 100       54 if (isroot($cur)) {
398 4         26 $root = $cur
399             } else {
400 16         425 $cur = Cwd::abs_path catdir($cur, updir)
401             }
402             }
403 9         45 $root;
404             }
405              
406             sub wire_getopt {
407 0     0 1 0 my $hash = shift;
408 0 0       0 confess 'Needs hash' unless ref $hash eq 'HASH';
409              
410 0         0 my @out;
411 0         0 my @recipe = _read_recipe($IDX_GETOPT_SUFFIX);
412 0         0 for (my $i = 0; $i < @recipe; $i += 2) {
413 0         0 push @out, $recipe[$i] . $recipe[$i + 1] => \$hash->{$recipe[$i]}
414             }
415 0         0 @out;
416             }
417              
418             =head2 Methods
419              
420             =over 1
421              
422             =item save_to
423              
424             Save the configuration to a file. This will also update the inner root
425             reference, so the intsance will point to the saved file.
426              
427             =cut
428              
429             sub save_to {
430 3     3 1 37 my($self, $root) = @_;
431              
432 3         65 my $orig_root = delete $self->{_root};
433              
434             # YAML::Tiny does not like blessed items. I could unbless with
435             # Data::Structure::Util, or easily do a shallow copy
436 3         25 my $yaml = YAML::Tiny::Dump {%$self};
437              
438 3         2963 eval {
439 3         26 my $enc_root = encode(locale_fs => $root);
440 3 50 33     244 -e $enc_root or make_path $enc_root
441             or die "Cannot mkdir $root: $!";
442 3 50   3   303 open(my $out,
  3         7  
  3         21  
  3         44  
443             '>:encoding(locale)',
444             encode(locale_fs => catfile($root, $CONF_NAME)),
445             ) or die "Cannot open $CONF_NAME in $root: $!";
446 3         3485 print $out $yaml;
447 3         191 close $out;
448              
449 3         118 $self->{_root} = $root;
450             };
451 3 50       21 $@ and do {
452 0           $self->{_root} = $orig_root;
453 0           croak $@ =~ s/ at.*$//sr;
454             }
455             }
456              
457             =back
458              
459             =cut
460              
461             use overload
462 4   50 4   498 '""' => sub { 'PFT::Conf[ ' . (shift->{_root} || '?') . ' ]' },
463 3     3   1471 ;
  3         1194  
  3         56  
464              
465             1;