File Coverage

blib/lib/Config/Std.pm
Criterion Covered Total %
statement 253 277 91.3
branch 71 96 73.9
condition 32 61 52.4
subroutine 34 41 82.9
pod n/a
total 390 475 82.1


line stmt bran cond sub pod time code
1             package Config::Std;
2              
3             our $VERSION = '0.903';
4              
5 10     10   148218 use 5.007_003; # Testing with 5.8.1 since that's cpanm minimum :-)
  10         93  
6 10     10   62 use strict;
  10         22  
  10         308  
7 10     10   65 use warnings;
  10         28  
  10         1174  
8              
9             my %global_def_sep;
10             my %global_inter_gap;
11              
12             sub import {
13 9     9   116 my ($package, $opt_ref) = @_;
14 9         31 my $caller = caller();
15 9         30 $global_def_sep{$caller} = $opt_ref->{def_sep};
16 9         30 $global_inter_gap{$caller} = $opt_ref->{def_gap};
17 9         43 for my $sub_name (qw( read_config write_config )) {
18 18   66     245 $opt_ref->{$sub_name} ||= $sub_name;
19             }
20 10     10   69 no strict "refs";
  10         21  
  10         1162  
21 9         30 *{$caller.'::'.$opt_ref->{read_config}} = \&Config::Std::Hash::read_config;
  9         78  
22 9         30 *{$caller.'::'.$opt_ref->{write_config}} = \&Config::Std::Hash::write_config;
  9         1382  
23             }
24              
25             package Config::Std::Gap;
26 10     10   5343 use Class::Std;
  10         152271  
  10         57  
27             {
28 39     39   73 sub serialize { return "\n" }
29       26     sub update {}
30       0     sub extend {}
31       42     sub copy_to {}
32             }
33              
34             package Config::Std::Comment;
35 10     10   1866 use Class::Std;
  10         29  
  10         58  
36             {
37             my %text_of : ATTR( :init_arg );
38              
39             sub serialize {
40 0     0   0 my ($self) = @_;
41 0         0 return $text_of{ident $self};
42             }
43              
44             sub append_comment {
45 0     0   0 my ($self, $new_text) = @_;
46 0         0 $text_of{ident $self} .= $new_text;
47             }
48              
49       0     sub update {}
50       0     sub extend {}
51       0     sub copy_to {}
52             }
53              
54             package Config::Std::Keyval;
55 10     10   2485 use Class::Std;
  10         29  
  10         41  
56             {
57             my %key_of : ATTR( :init_arg :get );
58             my %vals_of : ATTR;
59             my %deleted_of : ATTR;
60              
61             sub BUILD {
62 53     53   1935 my ($self, $ident, $arg_ref) = @_;
63              
64 53         220 $vals_of{$ident} = [ { %{$arg_ref} } ];
  53         265  
65             }
66              
67             my %SEPARATOR = ( ':' => ': ', '=' => ' = ' );
68              
69 10     10   1953 use Carp;
  10         27  
  10         6877  
70              
71             sub serialize {
72 36     36   64 my ($self, $def_sep, $block_name) = @_;
73 36         53 my $ident = ident $self;
74              
75 36 50       69 return "" if $deleted_of{$ident};
76              
77 36         71 my ($key, $vals) = ($key_of{$ident}, $vals_of{$ident});
78              
79 36         57 my $keyspace = q{ } x length($key);
80              
81 36         40 my $serialization = q{};
82              
83 36         42 for my $n (0..$#{$vals}) {
  36         70  
84 47         56 my ($val,$sep,$comm) = @{$vals->[$n]}{qw(val sep comm)};
  47         93  
85              
86 47         62 my $val_type = ref $val;
87 47 50 66     90 croak qq{Can't save \L$val_type\E ref as value for key {'$block_name'}{'$key'} (only scalars or array refs)}
88             if $val_type && $val_type ne 'ARRAY';
89              
90 47   66     97 $sep = $SEPARATOR{$sep || $def_sep};
91              
92 47 100       100 my @vals = $val_type eq 'ARRAY' ? @{$val} : $val;
  1         2  
93 47         117 s/ (?!\Z) \n /\n$keyspace$sep/gxms for @vals;
94              
95 47   100     118 $serialization .= $comm || q{};
96              
97 47         73 $serialization .= join q{}, map {"$key$sep$_\n"} @vals;
  51         169  
98             }
99              
100 36         93 return $serialization;
101             }
102              
103             sub update {
104 25     25   33 my ($self, $hash_ref, $updated_ref) = @_;
105 25         37 my $ident = ident $self;
106              
107 25         39 my $key = $key_of{$ident};
108              
109 25 50       45 if (!exists $hash_ref->{$key}) {
110 0         0 $deleted_of{$ident} = 1;
111             }
112             else {
113 25         31 my $val = $hash_ref->{$key};
114 25 100       51 my @newvals = ref $val eq 'ARRAY' ? @{$val} : $val;
  2         7  
115 25         45 for my $n (0..$#newvals) {
116 36         63 $vals_of{$ident}[$n]{val} = $newvals[$n];
117             }
118 25         30 splice @{$vals_of{$ident}}, scalar @newvals;
  25         44  
119             }
120              
121 25         43 $updated_ref->{$key} = 1;
122              
123 25         46 return 1;
124             }
125              
126             sub copy_to {
127 41     41   61 my ($self, $hash_ref) = @_;
128 41         64 my $ident = ident $self;
129 41         49 my @vals = map $_->{val}, @{$vals_of{$ident}};
  41         112  
130 41 100       144 $hash_ref->{$key_of{$ident}} = @vals > 1 ? \@vals : $vals[0];
131             }
132              
133             sub multivalue {
134 17     17   38 my ($self, $sep, $val, $comm) = @_;
135 17         21 push @{$vals_of{ident $self}}, {val=>$val, sep=>$sep, comm=>$comm};
  17         88  
136             }
137             }
138              
139             package Config::Std::Block;
140 10     10   85 use Class::Std;
  10         22  
  10         53  
141             {
142             my %name_of : ATTR( :init_arg :get default => '' );
143             my %sep_count_of : ATTR;
144             my %precomm_of : ATTR( :init_arg default => '' );
145             my %parcomm_of : ATTR( :init_arg default => '' );
146             my %components_of : ATTR;
147             my %deleted_of : ATTR;
148             my %seen : ATTR;
149             my %is_first : ATTR( :init_arg default => '' );
150              
151             sub BUILD {
152 24     24   1166 my ($self, $ident) = @_;
153 24         44 @{$sep_count_of{$ident}}{':','='} = (0,0);
  24         87  
154 24         52 $components_of{$ident} = [];
155 24         62 $seen{$ident} = {};
156             }
157              
158             sub copy_to {
159 18     18   32 my ($self, $hash_ref) = @_;
160 18         31 my $ident = ident $self;
161              
162 18   50     90 my $keyvals = $hash_ref->{$name_of{$ident}} ||= {};
163              
164 18         26 for my $comp ( @{$components_of{$ident}} ) {
  18         32  
165 83         152 $comp->copy_to($keyvals);
166             }
167              
168 18         39 $hash_ref->{$name_of{$ident}} = $keyvals;
169             }
170              
171             sub serialize {
172 18     18   45 my ($self, $first, $caller, $post_gap, $inter_gap) = @_;
173 18         42 my $ident = ident $self;
174              
175 18 100       45 return q{} if $deleted_of{$ident};
176              
177 16   66     50 my $is_anon = $first && length($name_of{$ident}) == 0;
178              
179 16         22 my $serialization = q{};
180 16 100       30 if (!$is_anon) {
181             $serialization = ($precomm_of{$ident} || q{})
182             . "[$name_of{$ident}]"
183 11 100 50     68 . (defined $parcomm_of{$ident}?$parcomm_of{$ident}:q{})
184             . "\n";
185             }
186              
187 16         23 my $gds = $global_def_sep{$caller};
188             my $def_sep
189             = defined $gds ? $gds
190 16 50       44 : $sep_count_of{$ident}{':'} >= $sep_count_of{$ident}{'='} ? ':'
    100          
191             : '='
192             ;
193              
194 16 50 33     45 $self->ensure_gap() if $inter_gap && !$is_anon;
195              
196 16         20 for my $comp ( @{$components_of{$ident}} ) {
  16         31  
197 75         132 $serialization .= $comp->serialize($def_sep, $name_of{$ident});
198             }
199              
200 16         90 return $serialization;
201             }
202              
203             sub update {
204 13     13   23 my ($self, $hash_ref, $updated_ref) = @_;
205 13         20 my $ident = ident $self;
206              
207 13 100       28 if (!defined $hash_ref) {
208 2         24 $deleted_of{$ident} = 1;
209 2         6 return;
210             }
211              
212 11         14 for my $comp ( @{$components_of{$ident}} ) {
  11         20  
213 51 100       102 $comp->update($hash_ref, $updated_ref) or next;
214             }
215             }
216              
217             sub extend {
218 13     13   28 my ($self, $hash_ref, $updated_ref, $post_gap, $inter_gap) = @_;
219              
220             # Only the first occurrence of a block has new keys added...
221 13 50       34 return unless $is_first{ident $self};
222              
223 13         22 my $first = 1;
224             # RT 85956
225 13         25 for my $key ( sort grep {!$updated_ref->{$_}} keys %{$hash_ref}) {
  25         59  
  13         32  
226 0         0 my $value = $hash_ref->{$key};
227 0   0     0 my $separate = ref $value || $value =~ m/\n./xms;
228 0 0 0     0 $self->ensure_gap() if ($first ? $post_gap : $inter_gap)
    0          
229             || $separate;
230 0         0 $self->add_keyval($key, undef, $hash_ref->{$key});
231 0 0       0 $self->add_gap() if $separate;
232 0         0 $first = 0;
233             }
234             }
235              
236             sub ensure_gap {
237 13     13   21 my ($self) = @_;
238 13         35 my $comp_ref = $components_of{ident $self};
239 13 100 100     17 return if @{$comp_ref} && $comp_ref->[-1]->isa('Config::Std::Gap');
  13         94  
240 12         25 push @{$comp_ref}, Config::Std::Gap->new();
  12         38  
241             }
242              
243             sub add_gap {
244 44     44   70 my ($self) = @_;
245 44         53 push @{$components_of{ident $self}}, Config::Std::Gap->new();
  44         185  
246             }
247              
248             sub add_comment {
249 0     0   0 my ($self, $text) = @_;
250 0         0 my $comp_ref = $components_of{ident $self};
251 0 0 0     0 if ($comp_ref && @{$comp_ref} && $comp_ref->[-1]->isa('Config::Std::Comment') ) {
  0   0     0  
252 0         0 $comp_ref->[-1]->append_comment($text);
253             }
254             else {
255 0         0 push @{$comp_ref}, Config::Std::Comment->new({text=>$text});
  0         0  
256             }
257             }
258              
259             sub add_keyval {
260 70     70   159 my ($self, $key, $sep, $val, $comm) = @_;
261 70         136 my $ident = ident $self;
262              
263 70 100       162 $sep_count_of{$ident}{$sep}++ if $sep;
264              
265 70         98 my $seen = $seen{$ident};
266              
267 70 100       159 if ($seen->{$key}) {
268 17         50 $seen->{$key}->multivalue($sep, $val, $comm);
269 17         29 return;
270             }
271              
272 53         244 my $keyval
273             = Config::Std::Keyval->new({key=>$key, sep=>$sep, val=>$val, comm=>$comm});
274 53         1619 push @{$components_of{$ident}}, $keyval;
  53         90  
275 53         114 $seen->{$key} = $keyval;
276             }
277             }
278              
279             package Config::Std::Hash;
280 10     10   11501 use Class::Std;
  10         32  
  10         47  
281             {
282              
283 10     10   1091 use Carp;
  10         37  
  10         678  
284 10     10   79 use Fcntl ':flock'; # import LOCK_* constants
  10         21  
  10         18257  
285              
286             my %post_section_gap_for :ATTR;
287             my %array_rep_for :ATTR;
288             my %filename_for :ATTR;
289              
290             sub write_config (\[%$];$) {
291 8     8   872 my ($hash_ref, $filename) = @_;
292 8 100       30 $hash_ref = ${$hash_ref} if ref $hash_ref eq 'REF';
  1         2  
293              
294 8 100       56 $filename = $filename_for{$hash_ref} if @_<2;
295              
296 8 50       49 croak "Missing filename for call to write_config()"
297             unless $filename;
298              
299 8         18 my $caller = caller;
300              
301             my $inter_gap
302 8 50       35 = exists $global_inter_gap{$caller} ? $global_inter_gap{$caller}
303             : 1;
304             my $post_gap
305             = $post_section_gap_for{$hash_ref}
306 8   66     43 || (defined $global_inter_gap{$caller} ? $global_inter_gap{$caller}
307             : 1
308             );
309              
310             # Update existing keyvals in each block...
311 8         12 my %updated;
312 8         13 for my $block ( @{$array_rep_for{$hash_ref}} ) {
  8         27  
313 13         35 my $block_name = $block->get_name();
314 13   50     102 $block->update($hash_ref->{$block_name}, $updated{$block_name}||={});
315             }
316              
317             # Add new keyvals to the first section of block...
318 8         17 for my $block ( @{$array_rep_for{$hash_ref}} ) {
  8         23  
319 13         26 my $block_name = $block->get_name();
320 13         55 $block->extend($hash_ref->{$block_name}, $updated{$block_name},
321             $post_gap, $inter_gap
322             );
323             }
324              
325             # Add new blocks at the end...
326 8         17 for my $block_name ( sort grep {!$updated{$_}} keys %{$hash_ref} ) {
  17         48  
  8         22  
327 6         40 my $block = Config::Std::Block->new({name=>$block_name});
328 6         1100 my $subhash = $hash_ref->{$block_name};
329 6         14 my $first = 1;
330             # RT 85956
331 6         12 for my $key ( sort keys %{$subhash} ) {
  6         27  
332 13 100       37 if (!defined $subhash->{$key}) {
333 1         244 croak "Can't save undefined value for key {'$block_name'}{'$key'} (only scalars or array refs)";
334             }
335 12         17 my $value = $subhash->{$key};
336 12   100     55 my $separate = ref $value || $value =~ m/\n./xms;
337 12 100 100     61 $block->ensure_gap() if ($first ? $post_gap : $inter_gap)
    100          
338             || $separate;
339 12         436 $block->add_keyval($key, undef, $value);
340 12 100       29 $block->add_gap() if $separate;
341 12         94 $first = 0;
342             }
343 5         13 $block->ensure_gap();
344 5         158 push @{$array_rep_for{$hash_ref}}, $block;
  5         15  
345             }
346              
347 7 50       356 open my $fh, '>', $filename
348             or croak "Can't open config file '$filename' for writing (\L$!\E)";
349              
350 7 100 33     49 flock($fh,LOCK_EX|LOCK_NB)
351             || croak "Can't write to locked config file '$filename'"
352             if ! ref $filename;
353              
354 7         13 my $first = 1;
355 7         9 for my $block ( @{$array_rep_for{$hash_ref}} ) {
  7         27  
356 18         19 print {$fh} $block->serialize($first, scalar caller, $post_gap);
  18         53  
357 18         37 $first = 0;
358             }
359              
360 7 100       152 flock($fh,LOCK_UN) if ! ref $filename;
361              
362 7         291 return 1;
363             }
364              
365             sub read_config ($\[%$]) {
366 7     7   1049 my ($filename, $var_ref, $opt_ref) = @_;
367 7   50     32 my $var_type = ref($var_ref) || q{};
368 7         14 my $hash_ref;
369 7 100 66     45 if ($var_type eq 'SCALAR' && !defined ${$var_ref} ) {
  1 50       4  
370 1         2 ${$var_ref} = $hash_ref = {};
  1         1  
371             }
372             elsif ($var_type eq 'HASH') {
373 6         14 $hash_ref = $var_ref;
374             }
375             else {
376 0         0 croak q{Scalar second argument to 'read_config' must be empty};
377             }
378              
379 7         42 bless $hash_ref, 'Config::Std::Hash';
380              
381 7         25 my $blocks = $array_rep_for{$hash_ref}
382             = _load_config_for($filename, $hash_ref);
383              
384 7         21 for my $block ( @{$blocks} ) {
  7         16  
385 18         41 $block->copy_to($hash_ref);
386             }
387              
388 7         21 $filename_for{$hash_ref} = $filename;
389              
390             # Remove initial empty section if no data...
391 7 100       13 if (!keys %{ $hash_ref->{q{}} }) {
  7         26  
392 2         4 delete $hash_ref->{q{}};
393             }
394              
395 7         36 return 1;
396             }
397              
398             sub _load_config_for {
399 7     7   23 my ($filename, $hash_ref) = @_;
400              
401 7 50   1   167 open my $fh, '<', $filename
  1         5  
  1         1  
  1         5  
402             or croak "Can't open config file '$filename' (\L$!\E)";
403 7 100 33     484 flock($fh,LOCK_SH|LOCK_NB)
404             || croak "Can't read from locked config file '$filename'"
405             if !ref $filename;
406 7         14 my $text = do{local $/; <$fh>};
  7         24  
  7         105  
407 7 100       43 flock($fh,LOCK_UN) if !ref $filename;
408              
409 7         71 my @config_file = Config::Std::Block->new({ name=>q{}, first=>1 });
410 7         1138 my $comment = q{};
411 7         17 my %seen;
412              
413             # Start tracking whether section markers have gaps after them...
414 7         50 $post_section_gap_for{$hash_ref} = 0;
415              
416 7         20 for ($text) {
417 7         26 pos = 0;
418 7         35 while (pos() < length() ) {
419             # Gap...
420 113 100       766 if (m/\G (?: [^\S\n]* (?:\n|\z)+)/gcxms) {
    100          
    100          
    50          
421             ### Found gap
422 42 50       81 $config_file[-1]->add_comment($comment) if $comment;
423 42         107 $config_file[-1]->add_gap();
424 42         1959 $comment = q{};
425             }
426              
427             # Comment...
428             elsif (m/\G (\s* [#;] [^\n]* (?:\n|\z) )/gcxms) {
429             ### Found comment: $1
430 2         6 $comment .= $1;
431             }
432              
433             # Block...
434             elsif (m/\G ([^\S\n]*) [[] ( [^]\n]* ) []] ( ([^\S\n]*) [#;] [^\n]* )? [^\S\n]* (?:\n|\z)/gcxms) {
435 11         51 my ($pre, $name, $parcomm, $ws) = ($1, $2, $3, $4);
436             ### Found block: $name
437 11 50       31 if ($parcomm) {
438 0         0 $pre = 2 + length($pre) + length($name) + length($ws);
439 0 0       0 if (m/\G ( (?: \n? [ ]{$pre,} [#] [^\n]* )+ )/gcxms) {
440 0         0 $parcomm .= "\n$1";
441             }
442             }
443             push @config_file,
444             Config::Std::Block->new({
445             name => $name,
446             precomm => $comment,
447             parcomm => $parcomm,
448 11         90 first => !$seen{$name}++,
449             });
450 11         535 $comment = q{};
451              
452             # Check for trailing gap...
453 11 50       73 $post_section_gap_for{$hash_ref}
454             += m/\G (?= [^\S\n]* (?:\n|\z) )/xms ? +1 : -1;
455             }
456              
457             # Key/value...
458             elsif (m/\G [^\S\n]* ([^=:\n]+?) [^\S\n]* ([:=] [^\S\n]*) ([^\n]*) (?:\n|\z)/gcxms) {
459 58         204 my ($key, $sep, $val) = ($1, $2, $3);
460              
461 58         84 my $pure_sep = $sep;
462 58         256 $pure_sep =~ s/\s*//g;
463              
464             # Continuation lines...
465 58         94 my $continued = 0;
466 58   66     804 while (m/\G [^\S\n]* \Q$sep\E ([^\n]*) (?:\n|\z) /gcxms
467             || m/\G [^\S\n]* \Q$pure_sep\E ([^\n]*) (?:\n|\z) /gcxms
468             ) {
469 8         21 $val .= "\n$1";
470 8         47 $continued = 1;
471             }
472              
473 58 100       345 $val =~ s/\A \s*|\s* \z//gxms if !$continued;
474              
475             ### Found kv: $key, $val
476              
477 58         174 $config_file[-1]->add_keyval($key, $pure_sep, $val,
478 58         144 $comment); $comment = q{}; }
479              
480             # Mystery...
481             else {
482 0         0 my ($problem) = m/\G ([^\n]{10,40}|.{10}) /gcxms;
483 0         0 die "Error in config file '$filename' near:\n\n\t$problem\n";
484             }
485             }
486             }
487              
488 7         96 return \@config_file;
489             }
490              
491             sub DEMOLISH {
492 7     7   5331 my ($self, $ident) = @_;
493              
494             # Do nothing. Defined to suppress use warnings in Class::Std destructor.
495             }
496              
497              
498             }
499              
500              
501             1; # Magic true value required at end of module
502             __END__