File Coverage

blib/lib/Config/Model/Backend/Any.pm
Criterion Covered Total %
statement 102 110 92.7
branch 34 38 89.4
condition n/a
subroutine 14 16 87.5
pod 6 8 75.0
total 156 172 90.7


line stmt bran cond sub pod time code
1             #
2             # This file is part of Config-Model
3             #
4             # This software is Copyright (c) 2005-2022 by Dominique Dumont.
5             #
6             # This is free software, licensed under:
7             #
8             # The GNU Lesser General Public License, Version 2.1, February 1999
9             #
10             package Config::Model::Backend::Any 2.153; # TRIAL
11              
12 9     9   1876 use v5.20;
  9         34  
13              
14 9     9   110 use Carp;
  9         22  
  9         535  
15 9     9   77 use Config::Model::Exception;
  9         30  
  9         232  
16 9     9   54 use Mouse;
  9         33  
  9         89  
17              
18 9     9   2895 use File::Path;
  9         22  
  9         569  
19 9     9   61 use Log::Log4perl qw(get_logger :levels);
  9         35  
  9         59  
20              
21 9     9   1302 use feature qw/postderef signatures/;
  9         25  
  9         1238  
22 9     9   73 no warnings qw/experimental::postderef experimental::signatures/;
  9         47  
  9         14762  
23              
24             my $logger = get_logger("Backend");
25              
26             has 'name' => ( is => 'ro', default => 'unknown', );
27             has [qw/annotation auto_create auto_delete/] => ( is => 'ro', isa => 'Bool', default => 0 );
28             has 'node' => (
29             is => 'ro',
30             isa => 'Config::Model::Node',
31             weak_ref => 1,
32             required => 1,
33             handles => [ qw/show_message instance get_element_names/],
34             );
35              
36 208     208 0 1099 sub skip_open { return 0; }
37              
38             sub read {
39 0     0 1 0 my $self = shift;
40 0         0 my $err = "Internal error: read not defined in backend $self->{name}.";
41 0         0 $logger->error($err);
42 0         0 croak $err;
43             }
44              
45             sub write {
46 0     0 1 0 my $self = shift;
47 0         0 my $err = "Internal error: write not defined in backend $self->{name}.";
48 0         0 $logger->error($err);
49 0         0 croak $err;
50             }
51              
52             sub read_global_comments {
53 54     54 1 194 my $self = shift;
54 54         118 my $lines = shift;
55 54         107 my $cc = shift; # comment character(s)
56              
57 54 100       219 my $cc_re = length $cc > 1 ? "[$cc]" : $cc;
58 54         174 my @global_comments;
59             my @global_comment_lines;
60              
61 54         272 while ( defined( my $l = shift @$lines ) ) {
62 121 100       959 next if $l =~ /^$cc_re{2}/; # remove comments added by Config::Model
63 54         178 unshift @$lines, $l;
64 54         154 last;
65             }
66 54         224 while ( defined( my $l = shift @$lines ) ) {
67 76 100       451 next if $l =~ /^\s*$/; # remove empty lines
68 54         160 unshift @$lines, $l;
69 54         114 last;
70             }
71              
72 54         217 while ( defined( my $l = shift @$lines ) ) {
73 103         263 chomp $l;
74              
75 103         767 my ( $data, $comment ) = split /\s*$cc_re\s?/, $l, 2;
76              
77 103 100       308 if (defined $comment) {
78 50         112 push @global_comment_lines, $l;
79 50         110 push @global_comments, $comment;
80             }
81              
82 103 100       397 if ( $l =~ /^\s*$/ ) {
83             # we indeed had global comments which are now finished by
84             # a blank line. Store them and bail out
85 23 50       111 if (@global_comments) {
86 23         247 $self->node->annotation(@global_comments);
87 23         224 $logger->debug("Setting global comment with @global_comments on ", $self->node->name);
88             }
89             # stop global comment at first blank line
90 23         299 last;
91             }
92 80 100       285 if ( $data ) {
93             # The comment found is not global, put back line and any captured comment
94 31         87 unshift @$lines, @global_comment_lines, $l;
95              
96             # stop global comment
97 31         138 last;
98             }
99             }
100             }
101              
102             sub associates_comments_with_data {
103 53     53 1 133 my $self = shift;
104 53         141 my $lines = shift;
105 53         109 my $cc = shift; # comment character(s)
106              
107 53 100       186 my $cc_re = length $cc > 1 ? "[$cc]" : $cc;
108 53         120 my @result;
109             my @comments;
110 53         173 foreach my $l (@$lines) {
111 688 100       2174 next if $l =~ /^$cc_re{2}/; # remove comments added by Config::Model
112 685         1154 chomp $l;
113              
114 685         2576 my ( $data, $comment ) = split /\s*$cc_re\s?/, $l, 2;
115 685 100       1574 push @comments, $comment if defined $comment;
116              
117 685 100       1312 next unless defined $data;
118 570         1238 $data =~ s/^\s+//g;
119 570         1379 $data =~ s/\s+$//g;
120              
121 570 100       1129 if ($data) {
122 402         623 my $note = '';
123 402 100       1008 $note = join( "\n", @comments ) if @comments;
124 402         1823 $logger->trace("associates_comments_with_data: '$note' with '$data'");
125 402         3518 push @result, [ $data, $note ];
126 402         969 @comments = ();
127             }
128             }
129              
130 53 50       370 return wantarray ? @result : \@result;
131              
132             }
133              
134             sub write_global_comment {
135 28     28 0 140 goto &write_global_comments;
136             }
137              
138 28     28 1 61 sub write_global_comments ($self, $cc) {
  28         82  
  28         64  
  28         52  
139 28 50       103 croak "write_global_comments: no comment char specified" unless $cc;
140              
141             # no need to mention 'cme list' if current application is found
142 28         249 my $app = $self->node->instance->application ;
143 28         72 my $extra = '' ;
144 28 100       94 if (not $app) {
145 13         44 $extra = "$cc$cc Run 'cme list' to get the list of applications"
146             . " available on your system\n";
147 13         22 $app = '<application>';
148             }
149              
150 28         183 my $res = "$cc$cc This file was written by cme command.\n"
151             . "$cc$cc You can run 'cme edit $app' to modify this file.\n"
152             . $extra
153             . "$cc$cc You may also modify the content of this file with your favorite editor.\n\n";
154              
155             # write global comment
156 28         124 my $global_note = $self->node->annotation;
157 28 100       131 if ($global_note) {
158 11         112 for ( split /\n/, $global_note ) { $res .= "$cc $_\n" }
  26         109  
159 11         34 $res .= "\n";
160             }
161              
162 28         228 return $res;
163             }
164              
165             # $cc can be undef when writing a list on a single line
166 157     157 1 265 sub write_data_and_comments ( $self, $cc, @data_and_comments ) {
  157         246  
  157         252  
  157         263  
  157         226  
167 157         258 my $res = '';
168 157         427 while (@data_and_comments) {
169 157         408 my ( $d, $c ) = splice @data_and_comments, 0, 2;
170 157 100       373 if ($c) {
171 61         272 for (split /\n/, $c ) { $res .= "$cc $_\n" }
  67         245  
172             }
173 157 50       692 $res .= "$d\n" if defined $d;
174             }
175 157         597 return $res;
176             }
177              
178             __PACKAGE__->meta->make_immutable;
179              
180             1;
181              
182             # ABSTRACT: Virtual class for other backends
183              
184             __END__
185              
186             =pod
187              
188             =encoding UTF-8
189              
190             =head1 NAME
191              
192             Config::Model::Backend::Any - Virtual class for other backends
193              
194             =head1 VERSION
195              
196             version 2.153
197              
198             =head1 SYNOPSIS
199              
200             package Config::Model::Backend::Foo ;
201             use Mouse ;
202              
203             extends 'Config::Model::Backend::Any';
204              
205             # mandatory
206             sub read {
207             my $self = shift ;
208             my %args = @_ ;
209              
210             # args are:
211             # root => './my_test', # fake root directory, used for tests
212             # config_dir => /etc/foo', # absolute path
213             # file => 'foo.conf', # file name
214             # file_path => Path::Tiny object for './my_test/etc/foo/foo.conf'
215             # check => yes|no|skip
216              
217             return 0 unless $args{file_path}->exists ; # or die, your choice
218              
219             # read the file line by line
220             # we assume the file contain lines like 'key=value'
221             foreach ($args{file_path}->lines_utf8) {
222             chomp ; # remove trailing \n
223             s/#.*// ; # remove any comment
224             next unless /\S/; # skip blank line
225              
226             # $data is 'foo=bar' which is compatible with load
227             $self->node->load(steps => $_, check => $args{check} ) ;
228             }
229             return 1 ;
230             }
231              
232             # mandatory
233             sub write {
234             my $self = shift ;
235             my %args = @_ ;
236              
237             # args are:
238             # root => './my_test', # fake root directory, used for tests
239             # config_dir => /etc/foo', # absolute path
240             # file => 'foo.conf', # file name
241             # file_path => Path::Tiny object for './my_test/etc/foo/foo.conf'
242             # check => yes|no|skip
243              
244             # read the content of the configuration tree
245             my @lines;
246             foreach my $elt ($self->node->children) {
247             # read the value from element $elt
248             my $v = $self->node->grab_value($elt) ;
249              
250             # write value in file
251             push @lines,qq!$elt="$v"\n! if defined $v ;
252             }
253              
254             $args{file_path}->spew_utf8(@lines);
255             return 1;
256             }
257              
258             =head1 DESCRIPTION
259              
260             Some application have configuration files with a syntax which is not
261             supported by existing C<Config::Model::Backend::*> classes.
262              
263             In this case a new backend must be
264             written. C<Config::Model::Backend::Any> was created to facilitate this
265             task.
266              
267             The new backend class must use L<Mouse> and must extends (inherit)
268             C<Config::Model::Backend::Any>.
269              
270             =head1 How to write your own backend
271              
272             =head2 Declare the new backend in a node of the model
273              
274             As explained in L<Config::Model::BackendMgr/"Backend specification">, the
275             new backend must be declared as an attribute of a
276             L<Config::Model::Node> specification.
277              
278             Let's say your new backend is C<Config::Model::Backend::Foo>. This new backend
279             can be specified with:
280              
281             rw_config => {
282             backend => 'Foo' , # can also be 'foo'
283             config_dir => '/etc/cfg_dir'
284             file => 'foo.conf', # optional
285             }
286              
287             (The backend class name is constructed with C<ucfirst($backend_name)>)
288              
289             C<rw_config> can also have custom parameters that are passed
290             verbatim to C<Config::Model::Backend::Foo> methods:
291              
292             rw_config => {
293             backend => 'Foo' , # can also be 'foo'
294             config_dir => '/etc/cfg_dir'
295             file => 'foo.conf', # optional
296             my_param => 'my_value',
297             }
298              
299             C<Config::Model::Backend::Foo> class must inherit (extend)
300             L<Config::Model::Backend::Any> and is expected to provide the
301             following methods:
302              
303             =over
304              
305             =item read
306              
307             C<read> is called with the following parameters:
308              
309             %custom_parameters, # e.g. my_param => 'my_value' in the example above
310             object => $obj, # Config::Model::Node object
311             root => $root_dir, # fake root directory, used for tests
312             backend => $backend, # backend name
313             config_dir => $read_dir, # path below root
314             file => 'foo.conf', # file name
315             file_path => $full_name, # Path::Tiny object
316             check => [yes|no|skip]
317              
318             The L<IO::File> object is undef if the file cannot be read.
319              
320             This method must return 1 if the read was successful, 0 otherwise.
321              
322             Following the C<my_param> example above, C<%custom_parameters> contains
323             C< ( 'my_param' , 'my_value' ) >, so C<read()> is called with
324             C<root>, C<config_dir>, C<file_path> B<and>
325             C<< my_param => 'my_value' >>.
326              
327             =item write
328              
329             C<write> is called with the following parameters:
330              
331             %$custom_parameters, # e.g. my_param => 'my_value' in the example above
332             object => $obj, # Config::Model::Node object
333             root => $root_dir, # fake root directory, used for tests
334             auto_create => $auto_create, # boolean specified in backend declaration
335             auto_delete => $auto_delete, # boolean specified in backend declaration
336             backend => $backend, # backend name
337             config_dir => $write_dir, # override from instance
338             file => 'foo.conf', # file name
339             file_path => $full_name, # full file name (root+path+file)
340             write => 1, # always
341             check => [ yes|no|skip] ,
342             backup => [ undef || '' || suffix ] # backup strategy required by user
343              
344             The L<IO::File> object is undef if the file cannot be written to.
345              
346             This method must return 1 if the write was successful, 0 otherwise
347              
348             =back
349              
350             =head2 How to test your new backend
351              
352             Using L<Config::Model::Tester>, you can test your model with your
353             backend following the instructions given in L<Config::Model::Tester>.
354              
355             You can also test your backend with a minimal model (and
356             L<Config::Model::Tester>). In this case, you need to specify
357             a small model to test in a C<*-test-conf.pl> file.
358             See the
359             L<IniFile backend test|https://github.com/dod38fr/config-model/blob/master/t/model_tests.d/backend-ini-test-conf.pl>
360             for an example and its
361             L<examples files|https://github.com/dod38fr/config-model/tree/master/t/model_tests.d/backend-ini-examples>.
362              
363             =head1 CONSTRUCTOR
364              
365             =head2 new
366              
367             The constructor should be used only by L<Config::Model::Node>.
368              
369             Parameter:
370              
371             =over
372              
373             =item node
374              
375             Calling node object. Node ref is weakened,
376              
377             =item name
378              
379             Backend name
380              
381             =item auto_create
382              
383             Boolean. Set to true to create the configuration file if this one is
384             missing (default 0)
385              
386             =item auto_delete
387              
388             Boolean. Set to true to remove the configuration file if this one no
389             longer contain configuration information. (default 0)
390              
391             =back
392              
393             =head1 Methods to override
394              
395             =head2 annotation
396              
397             Whether the backend supports reading and writing annotation (a.k.a
398             comments). Default is 0. Override this method to return 1 if your
399             backend supports annotations.
400              
401             =head2 read
402              
403             Read the configuration file. This method must be overridden.
404              
405             =head2 write
406              
407             Write the configuration file. This method must be overridden.
408              
409             =head1 Methods
410              
411             =head2 node
412              
413             Return the node (a L<Config::Model::Node>) holding this backend.
414              
415             =head2 instance
416              
417             Return the instance (a L<Config::Model::Instance>) holding this configuration.
418              
419             =head2 show_message
420              
421             Parameters: C<( string )>
422              
423             Show a message to STDOUT (unless overridden).
424             Delegated to L<Config::Model::Instance/"show_message">.
425              
426             =head2 read_global_comments
427              
428             Parameters:
429              
430             =over
431              
432             =item *
433              
434             array ref of string containing the lines to be parsed
435              
436             =item *
437              
438             A string to specify how a comment is started. Each
439             character is recognized as a comment starter (e.g 'C<#;>' allow a
440             comment to begin with 'C<#>' or 'C<;>')
441              
442             =back
443              
444             Read the global comments (i.e. the first block of comments until the
445             first blank or non comment line) and store them as root node
446             annotation. Note that the global comment must be separated from the
447             first data line by a blank line.
448              
449             Example:
450              
451             $self->read_global_comments( \@lines, ';');
452             $self->read_global_comments( \@lines, '#;');
453              
454             =head2 associates_comments_with_data
455              
456             Parameters:
457              
458             =over
459              
460             =item *
461              
462             array ref of string containing the lines to be parsed
463              
464             =item *
465              
466             A string to specify how a comment is started. Each
467             character is recognized as a comment starter (e.g 'C<#;>' allow a
468             comment to begin with 'C<#>' or 'C<;>')
469              
470             =back
471              
472             This method extracts comments from the passed lines and associate
473             them with actual data found in the file lines. Data is associated with
474             comments preceding or on the same line as the data. Returns a list of
475             [ data, comment ].
476              
477             Example:
478              
479             my @lines = (
480             '# Foo comments',
481             'foo= 1',
482             'Baz = 0 # Baz comments'
483             );
484             my @res = $self->associates_comments_with_data( \@lines, '#')
485             # @res is:
486             # ( [ 'foo= 1', 'Foo comments' ] , [ 'Baz = 0' , 'Baz comments' ] )
487              
488             =head2 write_global_comments
489              
490             Return a string containing global comments using data from
491             configuration root annotation.
492              
493             Requires one parameter: comment_char (e.g "#" or '//' )
494              
495             Example:
496              
497             my $str = $self->write_global_comments('#')
498              
499             =head2 write_data_and_comments
500              
501             Returns a string containing comments (stored in annotation) and
502             corresponding data. Comments are written before the data. If a data is
503             undef, the comment is written on its own line.
504              
505             Positional parameters are C<( comment_char , data1, comment1, data2, comment2 ...)>
506              
507             Example:
508              
509             print $self->write_data_and_comments('#', 'foo', 'foo comment', undef, 'lone comment','bar')
510             # returns "# foo comment\nfoo\n#lon
511              
512             Use C<undef> as comment char if comments are not supported by the
513             syntax of the configuration file. Comments will then be dropped.
514              
515             =head1 Replacing a custom backend
516              
517             Custom backend are now deprecated and must be replaced with a class inheriting this module.
518              
519             Please:
520              
521             =over
522              
523             =item *
524              
525             Rename your class to begin with C<Config::Model::Backend::>
526              
527             =item *
528              
529             Add C<use Mouse ;> and C<extends 'Config::Model::Backend::Any';> in the header of your custom class.
530              
531             =item *
532              
533             Add C<my $self = shift;> as the beginning of C<read> and C<write> functions... well... methods.
534              
535             =back
536              
537             Here's an L<example of such a change|https://github.com/dod38fr/config-model/commit/c3b7007ad386cb2356c5ac1499fe51bdf492b19a>.
538              
539             =head1 AUTHOR
540              
541             Dominique Dumont, (ddumont at cpan dot org)
542              
543             =head1 SEE ALSO
544              
545             L<Config::Model>,
546             L<Config::Model::BackendMgr>,
547             L<Config::Model::Node>,
548              
549             =head1 AUTHOR
550              
551             Dominique Dumont
552              
553             =head1 COPYRIGHT AND LICENSE
554              
555             This software is Copyright (c) 2005-2022 by Dominique Dumont.
556              
557             This is free software, licensed under:
558              
559             The GNU Lesser General Public License, Version 2.1, February 1999
560              
561             =cut