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.401';
71              
72 2     2   2088 use strict;
  2         4  
  2         65  
73              
74 2     2   9 no warnings qw(uninitialized);
  2         3  
  2         65  
75              
76 2     2   482 use Config::Neat;
  2         4  
  2         101  
77 2         177 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   15 read_file);
  2         4  
81 2     2   13 use File::Spec::Functions qw(rel2abs);
  2         3  
  2         115  
82 2     2   13 use File::Basename qw(dirname);
  2         4  
  2         117  
83 2     2   1359 use Storable qw(dclone);
  2         6201  
  2         136  
84 2     2   16 use Tie::IxHash;
  2         3  
  2         7496  
85              
86             #
87             # Initialize object
88             #
89             sub new {
90 3     3 0 731 my ($class) = @_;
91              
92 3         16 my $self = {
93             cfg => Config::Neat->new(),
94             };
95              
96 3         7 bless $self, $class;
97 3         7 return $self;
98             }
99              
100             sub init {
101 61     61 0 115 my ($self) = @_;
102              
103 61         329 $self->{cache} = {};
104 61         163 $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 151221 my ($self, $filename, $binmode) = @_;
112              
113 61         188 $self->init;
114 61         111 $self->{binmode} = $binmode;
115              
116 61         152 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   187 my ($self, $filename) = @_;
129              
130 92         241 $filename = rel2abs($filename);
131 92         1105 return $self->_parse(read_file($filename, $self->{binmode}), $filename);
132             }
133              
134             sub _parse {
135 92     92   262 my ($self, $nconf_text, $filename) = @_;
136              
137             # preserve current context
138 92         423 push @{$self->{saved_context}}, {
139             orig_data => $self->{orig_data},
140             fullpath => $self->{fullpath}
141 92         125 };
142              
143             # parse the file
144 92         344 my $data = $self->{cfg}->parse($nconf_text);
145              
146             # generate the local context for expand_data()
147 92         228 $self->{orig_data} = _clone($data);
148 92         457 $self->{fullpath} = rel2abs($filename);
149              
150             # process @inherit rules
151 92         4626 $data = $self->expand_data($data, $data, dirname($self->{fullpath}));
152              
153             # restore the context
154 86         168 my $context = pop @{$self->{saved_context}};
  86         187  
155 86         494 $self->{orig_data} = $context->{orig_data};
156 86         161 $self->{fullpath} = $context->{fullpath};
157              
158 86         561 return $data;
159             }
160              
161             sub find_next_node_to_expand {
162 1030     1030 0 2330 my ($self, $node) = @_;
163 1030 100       5257 if (is_hash($node)) {
164             map {
165 575         1620 my ($subnode, $key) = $self->find_next_node_to_expand($node->{$_});
  814         10522  
166 814 50       1808 return ($subnode, $key) if defined $subnode;
167 814 100 100     1964 return ($node, $_) if is_hash($node->{$_}) && exists $node->{$_}->{'@inherit'};
168             } keys %$node;
169             }
170 993         3814 return undef;
171             }
172              
173             sub expand_data {
174 185     185 0 524 my ($self, $base_node, $node, $dir) = @_;
175 185 50       632 if (is_hash($node)) {
176              
177             # expand child nodes
178 185         304 while (1) {
179 216         760 my ($subnode, $key) = $self->find_next_node_to_expand($node);
180 216 100       935 last unless $subnode;
181 37         117 $subnode->{$key} = $self->expand_data($base_node, $subnode->{$key}, $dir);
182             }
183              
184 179 100       499 if (exists $node->{'@inherit'}) {
185 65 50       350 die "The value of '\@inherit' must be a string or array" unless ref($node->{'@inherit'}) eq 'Config::Neat::Array';
186              
187 65         425 my @a = @{$node->{'@inherit'}};
  65         155  
188              
189 65         472 my $intermediate = new_ixhash;
190              
191 65         147 foreach my $from (@a) {
192 70         106 my $orig_from = $from;
193 70         244 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       167 $filename = '' if $filename eq '.';
197 70 50 66     211 die "Neither filename nor selector are specified" unless $filename or $selector;
198              
199             # normalize path and selector
200 70 100       201 my $fullpath = $filename eq '' ? $self->{fullpath} : rel2abs($filename, $dir); # make path absolute based on current context dir
201 70         778 $selector =~ s/^\///; # remove leading slash, if any
202              
203 70         173 $from = $fullpath.'#'.$selector;
204              
205             # make sure we don't have any infinite loops
206             map {
207 19 100       59 if ($from eq $_) {
208             my $err =
209             "Infinite loop detected in $self->{fullpath} at `\@inherit $orig_from`\n".
210 3         19 "\@include stack:\n", join("\n", @{$self->{include_stack}}), "\n\n";
  3         10  
211 3         51 die $err;
212             }
213 70         102 } @{$self->{include_stack}};
  70         161  
214              
215 67         112 push @{$self->{include_stack}}, $from;
  67         141  
216              
217 67         95 my $merge_node;
218 67 100       155 if (exists $self->{cache}->{$from}) {
219 8         20 $merge_node = _clone($self->{cache}->{$from});
220             } else {
221 59         93 my $merge_cfg;
222 59         122 my $merge_dir = $dir;
223 59 100       106 if ($filename) {
224 31         935 $merge_dir = dirname($fullpath);
225              
226 31 50       102 if (!exists $self->{cache}->{$fullpath}) {
227 31         83 $self->{cache}->{$fullpath} = $self->_parse_file($fullpath);
228             }
229 29         83 $merge_cfg = _clone($self->{cache}->{$fullpath});
230             } else {
231 28         59 $merge_cfg = _clone($base_node);
232             }
233              
234 57         194 $merge_node = $self->select_subnode($merge_cfg, $selector, $dir);
235 56         211 $merge_node = $self->expand_data($base_node, $merge_node, $merge_dir);
236 53         109 $self->{cache}->{$from} = _clone($merge_node);
237             }
238              
239 61         202 $intermediate = $self->merge_data($merge_node, $intermediate, $dir);
240 61         154 pop @{$self->{include_stack}};
  61         255  
241             }
242              
243 56         195 delete $node->{'@inherit'};
244              
245 56         1390 $node = $self->merge_data($node, $intermediate, $dir);
246             }
247             }
248              
249 170         778 return $node;
250             }
251              
252             sub select_subnode {
253 57     57 0 155 my ($self, $node, $selector, $dir) = @_;
254              
255 57 50       177 die "Bad selector syntax (double slash) in '$selector'" if $selector =~ m/\/{2,}/;
256 57 50       129 die "Bad selector syntax (leading slash) in '$selector'" if $selector =~ m/^\//;
257              
258 57 100       139 return _clone($node) if $selector eq '';
259              
260 34         100 my @a = split('/', $selector);
261              
262 34         62 my $result = $node;
263 34         77 foreach (@a) {
264 53 50       211 next if ($_ eq '');
265 53 100 66     122 if (is_hash($result) && exists $result->{$_}) {
266 52         354 $result = $result->{$_};
267             } else {
268 1         18 die "Can't find key '$_' in node (selector: '$selector')";
269             }
270             }
271 33         245 return _clone($result);
272             }
273              
274             sub _clone {
275 266     266   413 my $data = shift;
276 266 50       15323 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 541 my ($self, $data1, $data2, $dir) = @_;
283              
284 226 100 100     1328 if (is_hash($data1) && is_hash($data2)) {
    100 100        
285 128         315 my @keys = get_keys_in_order($data2, $data1);
286              
287 128         2177 foreach my $key (keys %$data1) {
288 120 100       1725 my $base_key = $key =~ m/^[\+\-](.*)$/ ? $1 : $key;
289              
290 120 100       313 if ($key =~ m/^-(.*)$/) {
291 11 50       40 die "Key '$key' contains bogus data; expected an empty or true value" unless $data1->{$key}->as_boolean;
292 11         46 delete $data1->{$key};
293 11         249 delete $data2->{$1};
294 11         209 next;
295             }
296              
297             # arrays are NOT merged by default; use `+key` to merge arrays
298 109 100       340 if (is_neat_array($data1->{$key})) {
299 73 100       212 if ($key =~ m/^\+(.*)$/) {
300 13 50 66     42 if ((!exists $data2->{$base_key} || is_neat_array($data2->{$base_key}))) {
301 13         50 $data1 = rename_ixhash_key($data1, $key, $base_key);
302 13         29 $key = $base_key;
303             }
304              
305 13 100 100     56 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         55 $data1->{$key} = Config::Neat::Array->new([$data1->{$key}]);
311             }
312             } else {
313 60         194 delete $data2->{$key};
314             }
315             }
316              
317             # hashes are merged by default; `+key { }` is the same as `key { }`
318 109 100 100     853 if (is_hash($data1->{$key}) && ($key =~ m/^\+(.*)$/)) {
319 5         21 $data1 = rename_ixhash_key($data1, $key, $base_key);
320 5         15 $key = $base_key;
321             }
322              
323 109 100 100     321 if (is_hash($data1->{$key}) && is_hash($data2->{$key})) {
324 11         41 my $offset = get_next_auto_key($data2->{$key});
325 11         42 $data1->{$key} = offset_keys($data1->{$key}, $offset);
326             }
327 109         494 $data1->{$key} = $self->merge_data($data1->{$key}, $data2->{$key}, $dir);
328             }
329              
330 128         1620 foreach my $key (keys %$data2) {
331 63 100 66     843 if (exists $data2->{$key} && !exists $data1->{$key}) {
332 42         410 $data1->{$key} = $data2->{$key};
333             }
334             }
335              
336 128 50       1558 $data1 = to_ixhash($data1) unless is_ixhash($data1);
337 128         319 $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       28 if (is_simple_array($data2)) {
342 8         24 unshift(@$data1, $data2);
343             } else {
344 2         9 unshift(@$data1, @$data2);
345             }
346             }
347              
348 226         791 return $data1;
349             }
350              
351             1;