File Coverage

lib/Config/Neat/Inheritable.pm
Criterion Covered Total %
statement 153 156 98.0
branch 56 68 82.3
condition 26 30 86.6
subroutine 18 19 94.7
pod 0 8 0.0
total 253 281 90.0


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Config::Neat::Inheritable - Config::Neat files with inheritance
4              
5             =head1 SYNOPSIS
6              
7             File 01.nconf:
8              
9             foo {
10             bar baz
11              
12             etc {
13             pwd 1
14             }
15             }
16              
17             abc def
18              
19             File 02.nconf:
20              
21             @inherit 01.nconf
22              
23             foo {
24             bar replace
25             }
26              
27             -abc
28              
29             Resulting data structure will be equivalent to:
30              
31             foo {
32             bar replace
33              
34             etc {
35             pwd 1
36             }
37             }
38              
39             Within C<@inherit>, you can use selectors as slash-delimited paths to the node
40             within a target file, for example:
41              
42             whatever {
43             @inherit 01.nconf#foo/etc
44             bar replace
45             }
46              
47             Resulting data structure will be equivalent to:
48              
49             whatever {
50             pwd 1
51             bar replace
52             }
53              
54             Multiple inheritance is supported; use '.' do denote the the current file:
55              
56             @inherit 01.nconf#foo 02.nconf#bar .#baz
57              
58             =head1 COPYRIGHT
59              
60             Copyright (C) 2012-2015 Igor Afanasyev
61              
62             =head1 SEE ALSO
63              
64             L
65              
66             =cut
67              
68             package Config::Neat::Inheritable;
69              
70             our $VERSION = '1.4';
71              
72 2     2   1834 use strict;
  2         5  
  2         57  
73              
74 2     2   9 no warnings qw(uninitialized);
  2         2  
  2         59  
75              
76 2     2   363 use Config::Neat;
  2         3  
  2         64  
77 2         149 use Config::Neat::Util qw(new_ixhash is_hash is_ixhash to_ixhash is_neat_array
78             is_simple_array get_next_auto_key offset_keys
79             get_keys_in_order reorder_ixhash rename_ixhash_key
80 2     2   10 read_file);
  2         4  
81 2     2   11 use File::Spec::Functions qw(rel2abs);
  2         3  
  2         108  
82 2     2   12 use File::Basename qw(dirname);
  2         3  
  2         103  
83 2     2   1075 use Storable qw(dclone);
  2         5164  
  2         103  
84 2     2   11 use Tie::IxHash;
  2         5  
  2         2860  
85              
86             #
87             # Initialize object
88             #
89             sub new {
90 3     3 0 597 my ($class) = @_;
91              
92 3         14 my $self = {
93             cfg => Config::Neat->new(),
94             };
95              
96 3         4 bless $self, $class;
97 3         6 return $self;
98             }
99              
100             sub init {
101 61     61 0 96 my ($self) = @_;
102              
103 61         252 $self->{cache} = {};
104 61         135 $self->{saved_context} = [];
105 61         131 $self->{include_stack} = [];
106             }
107              
108             # Given a file name, will read this file in the specified mode (UTF-8 by default),
109             # parse it and expand '@inherit' blocks
110             sub parse_file {
111 61     61 0 125485 my ($self, $filename, $binmode) = @_;
112              
113 61         169 $self->init;
114 61         92 $self->{binmode} = $binmode;
115              
116 61         133 return $self->_parse_file($filename);
117             }
118              
119             # Given a string representation of the config, returns a parsed tree
120             # with expanded '@inherit' blocks
121             sub parse {
122 0     0 0 0 my ($self, $nconf_text, $filename) = @_;
123 0         0 $self->init;
124 0         0 return $self->_parse($nconf_text, $filename);
125             }
126              
127             sub _parse_file {
128 92     92   151 my ($self, $filename) = @_;
129              
130 92         229 $filename = rel2abs($filename);
131 92         927 return $self->_parse(read_file($filename, $self->{binmode}), $filename);
132             }
133              
134             sub _parse {
135 92     92   196 my ($self, $nconf_text, $filename) = @_;
136              
137             # preserve current context
138 92         370 push @{$self->{saved_context}}, {
139             orig_data => $self->{orig_data},
140             fullpath => $self->{fullpath}
141 92         117 };
142              
143             # parse the file
144 92         314 my $data = $self->{cfg}->parse($nconf_text);
145              
146             # generate the local context for expand_data()
147 92         186 $self->{orig_data} = _clone($data);
148 92         359 $self->{fullpath} = rel2abs($filename);
149              
150             # process @inherit rules
151 92         3852 $data = $self->expand_data($data, $data, dirname($self->{fullpath}));
152              
153             # restore the context
154 86         129 my $context = pop @{$self->{saved_context}};
  86         159  
155 86         402 $self->{orig_data} = $context->{orig_data};
156 86         131 $self->{fullpath} = $context->{fullpath};
157              
158 86         451 return $data;
159             }
160              
161             sub find_next_node_to_expand {
162 1030     1030 0 1987 my ($self, $node) = @_;
163 1030 100       4342 if (is_hash($node)) {
164             map {
165 575         1348 my ($subnode, $key) = $self->find_next_node_to_expand($node->{$_});
  814         8707  
166 814 50       1489 return ($subnode, $key) if defined $subnode;
167 814 100 100     1677 return ($node, $_) if is_hash($node->{$_}) && exists $node->{$_}->{'@inherit'};
168             } keys %$node;
169             }
170 993         3190 return undef;
171             }
172              
173             sub expand_data {
174 185     185 0 748 my ($self, $base_node, $node, $dir) = @_;
175 185 50       522 if (is_hash($node)) {
176              
177             # expand child nodes
178 185         232 while (1) {
179 216         635 my ($subnode, $key) = $self->find_next_node_to_expand($node);
180 216 100       804 last unless $subnode;
181 37         106 $subnode->{$key} = $self->expand_data($base_node, $subnode->{$key}, $dir);
182             }
183              
184 179 100       450 if (exists $node->{'@inherit'}) {
185 65 50       315 die "The value of '\@inherit' must be a string or array" unless ref($node->{'@inherit'}) eq 'Config::Neat::Array';
186              
187 65         367 my @a = @{$node->{'@inherit'}};
  65         146  
188              
189 65         425 my $intermediate = new_ixhash;
190              
191 65         123 foreach my $from (@a) {
192 70         92 my $orig_from = $from;
193 70         219 my ($filename, $selector) = split('#', $from, 2);
194             # allow .#selector style to indicate the current file, since #selector
195             # without the leading symbol will be treated as a comment line
196 70 100       148 $filename = '' if $filename eq '.';
197 70 50 66     175 die "Neither filename nor selector are specified" unless $filename or $selector;
198              
199             # normalize path and selector
200 70 100       177 my $fullpath = $filename eq '' ? $self->{fullpath} : rel2abs($filename, $dir); # make path absolute based on current context dir
201 70         612 $selector =~ s/^\///; # remove leading slash, if any
202              
203 70         144 $from = $fullpath.'#'.$selector;
204              
205             # make sure we don't have any infinite loops
206             map {
207 19 100       55 if ($from eq $_) {
208             my $err =
209             "Infinite loop detected in $self->{fullpath} at `\@inherit $orig_from`\n".
210 3         14 "\@include stack:\n", join("\n", @{$self->{include_stack}}), "\n\n";
  3         7  
211 3         42 die $err;
212             }
213 70         86 } @{$self->{include_stack}};
  70         128  
214              
215 67         87 push @{$self->{include_stack}}, $from;
  67         120  
216              
217 67         75 my $merge_node;
218 67 100       121 if (exists $self->{cache}->{$from}) {
219 8         17 $merge_node = _clone($self->{cache}->{$from});
220             } else {
221 59         78 my $merge_cfg;
222 59         75 my $merge_dir = $dir;
223 59 100       78 if ($filename) {
224 31         778 $merge_dir = dirname($fullpath);
225              
226 31 50       91 if (!exists $self->{cache}->{$fullpath}) {
227 31         71 $self->{cache}->{$fullpath} = $self->_parse_file($fullpath);
228             }
229 29         65 $merge_cfg = _clone($self->{cache}->{$fullpath});
230             } else {
231 28         58 $merge_cfg = _clone($base_node);
232             }
233              
234 57         173 $merge_node = $self->select_subnode($merge_cfg, $selector, $dir);
235 56         169 $merge_node = $self->expand_data($base_node, $merge_node, $merge_dir);
236 53         79 $self->{cache}->{$from} = _clone($merge_node);
237             }
238              
239 61         174 $intermediate = $self->merge_data($merge_node, $intermediate, $dir);
240 61         124 pop @{$self->{include_stack}};
  61         200  
241             }
242              
243 56         162 delete $node->{'@inherit'};
244              
245 56         1100 $node = $self->merge_data($node, $intermediate, $dir);
246             }
247             }
248              
249 170         709 return $node;
250             }
251              
252             sub select_subnode {
253 57     57 0 132 my ($self, $node, $selector, $dir) = @_;
254              
255 57 50       160 die "Bad selector syntax (double slash) in '$selector'" if $selector =~ m/\/{2,}/;
256 57 50       109 die "Bad selector syntax (leading slash) in '$selector'" if $selector =~ m/^\//;
257              
258 57 100       123 return _clone($node) if $selector eq '';
259              
260 34         98 my @a = split('/', $selector);
261              
262 34         45 my $result = $node;
263 34         58 foreach (@a) {
264 53 50       187 next if ($_ eq '');
265 53 100 66     102 if (is_hash($result) && exists $result->{$_}) {
266 52         289 $result = $result->{$_};
267             } else {
268 1         19 die "Can't find key '$_' in node (selector: '$selector')";
269             }
270             }
271 33         206 return _clone($result);
272             }
273              
274             sub _clone {
275 266     266   348 my $data = shift;
276 266 50       12633 return ref($data) ? dclone($data) : $data;
277             }
278              
279             # merge tree structure from data2 into data1
280             # data1 is the one that may contain `-key` and `+key` entries
281             sub merge_data {
282 226     226 0 466 my ($self, $data1, $data2, $dir) = @_;
283              
284 226 100 100     1106 if (is_hash($data1) && is_hash($data2)) {
    100 100        
285 128         296 my @keys = get_keys_in_order($data2, $data1);
286              
287 128         1732 foreach my $key (keys %$data1) {
288 120 100       1395 my $base_key = $key =~ m/^[\+\-](.*)$/ ? $1 : $key;
289              
290 120 100       238 if ($key =~ m/^-(.*)$/) {
291 11 50       30 die "Key '$key' contains bogus data; expected an empty or true value" unless $data1->{$key}->as_boolean;
292 11         42 delete $data1->{$key};
293 11         206 delete $data2->{$1};
294 11         170 next;
295             }
296              
297             # arrays are NOT merged by default; use `+key` to merge arrays
298 109 100       300 if (is_neat_array($data1->{$key})) {
299 73 100       162 if ($key =~ m/^\+(.*)$/) {
300 13 50 66     34 if ((!exists $data2->{$base_key} || is_neat_array($data2->{$base_key}))) {
301 13         47 $data1 = rename_ixhash_key($data1, $key, $base_key);
302 13         28 $key = $base_key;
303             }
304              
305 13 100 100     48 if (is_simple_array($data1->{$key}) && exists $data2->{$base_key}) {
306             # we are about to merge two arrays; before doing so,
307             # if the accumulator array is a simple one, we need to convert it
308             # into an array, containing a single array ref, to maintain the
309             # 'array of arrays' structure
310 8         47 $data1->{$key} = Config::Neat::Array->new([$data1->{$key}]);
311             }
312             } else {
313 60         168 delete $data2->{$key};
314             }
315             }
316              
317             # hashes are merged by default; `+key { }` is the same as `key { }`
318 109 100 100     708 if (is_hash($data1->{$key}) && ($key =~ m/^\+(.*)$/)) {
319 5         18 $data1 = rename_ixhash_key($data1, $key, $base_key);
320 5         8 $key = $base_key;
321             }
322              
323 109 100 100     297 if (is_hash($data1->{$key}) && is_hash($data2->{$key})) {
324 11         35 my $offset = get_next_auto_key($data2->{$key});
325 11         37 $data1->{$key} = offset_keys($data1->{$key}, $offset);
326             }
327 109         423 $data1->{$key} = $self->merge_data($data1->{$key}, $data2->{$key}, $dir);
328             }
329              
330 128         1289 foreach my $key (keys %$data2) {
331 63 100 66     714 if (exists $data2->{$key} && !exists $data1->{$key}) {
332 42         336 $data1->{$key} = $data2->{$key};
333             }
334             }
335              
336 128 50       1293 $data1 = to_ixhash($data1) unless is_ixhash($data1);
337 128         271 $data1 = reorder_ixhash($data1, \@keys);
338             } elsif (is_neat_array($data1) && is_neat_array($data2)) {
339             # always push data as array ref, not as individual items
340             # to maintain 'array of arrays' structure
341 10 100       22 if (is_simple_array($data2)) {
342 8         21 unshift(@$data1, $data2);
343             } else {
344 2         6 unshift(@$data1, @$data2);
345             }
346             }
347              
348 226         657 return $data1;
349             }
350              
351             1;