File Coverage

lib/Config/Neat/Inheritable.pm
Criterion Covered Total %
statement 149 152 98.0
branch 56 68 82.3
condition 23 30 76.6
subroutine 18 19 94.7
pod 0 8 0.0
total 246 277 88.8


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.302';
71              
72 2     2   1900 use strict;
  2         5  
  2         67  
73              
74 2     2   13 no warnings qw(uninitialized);
  2         3  
  2         71  
75              
76 2     2   314 use Config::Neat;
  2         5  
  2         66  
77 2         168 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   14 read_file);
  2         3  
81 2     2   14 use File::Spec::Functions qw(rel2abs);
  2         17  
  2         97  
82 2     2   13 use File::Basename qw(dirname);
  2         5  
  2         112  
83 2     2   1273 use Storable qw(dclone);
  2         4587  
  2         100  
84 2     2   13 use Tie::IxHash;
  2         4  
  2         2460  
85              
86             #
87             # Initialize object
88             #
89             sub new {
90 3     3 0 676 my ($class) = @_;
91              
92 3         17 my $self = {
93             cfg => Config::Neat->new(),
94             };
95              
96 3         7 bless $self, $class;
97 3         6 return $self;
98             }
99              
100             sub init {
101 61     61 0 134 my ($self) = @_;
102              
103 61         156 $self->{cache} = {};
104 61         318 $self->{saved_context} = [];
105 61         173 $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 107452 my ($self, $filename, $binmode) = @_;
112              
113 61         244 $self->init;
114 61         136 $self->{binmode} = $binmode;
115              
116 61         189 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   212 my ($self, $filename) = @_;
129              
130 92         292 $filename = rel2abs($filename);
131 92         1180 return $self->_parse(read_file($filename, $self->{binmode}), $filename);
132             }
133              
134             sub _parse {
135 92     92   247 my ($self, $nconf_text, $filename) = @_;
136              
137             # preserve current context
138 92         417 push @{$self->{saved_context}}, {
139             orig_data => $self->{orig_data},
140             fullpath => $self->{fullpath}
141 92         161 };
142              
143             # parse the file
144 92         385 my $data = $self->{cfg}->parse($nconf_text);
145              
146             # generate the local context for expand_data()
147 92         285 $self->{orig_data} = _clone($data);
148 92         446 $self->{fullpath} = rel2abs($filename);
149              
150             # process @inherit rules
151 92         4809 $data = $self->expand_data($data, $data, dirname($self->{fullpath}));
152              
153             # restore the context
154 86         175 my $context = pop @{$self->{saved_context}};
  86         193  
155 86         194 $self->{orig_data} = $context->{orig_data};
156 86         472 $self->{fullpath} = $context->{fullpath};
157              
158 86         464 return $data;
159             }
160              
161             sub find_next_node_to_expand {
162 1030     1030 0 2436 my ($self, $node) = @_;
163 1030 100       5598 if (is_hash($node)) {
164             map {
165 575         1680 my ($subnode, $key) = $self->find_next_node_to_expand($node->{$_});
  814         10389  
166 814 50       1973 return ($subnode, $key) if defined $subnode;
167 814 100 66     2184 return ($node, $_) if is_hash($node->{$_}) && exists $node->{$_}->{'@inherit'};
168             } keys %$node;
169             }
170 993         3577 return undef;
171             }
172              
173             sub expand_data {
174 185     185 0 512 my ($self, $base_node, $node, $dir) = @_;
175 185 50       657 if (is_hash($node)) {
176              
177             # expand child nodes
178 185         317 while (1) {
179 216         848 my ($subnode, $key) = $self->find_next_node_to_expand($node);
180 216 100       944 last unless $subnode;
181 37         125 $subnode->{$key} = $self->expand_data($base_node, $subnode->{$key}, $dir);
182             }
183              
184 179 100       509 if (exists $node->{'@inherit'}) {
185 65 50       400 die "The value of '\@inherit' must be a string or array" unless ref($node->{'@inherit'}) eq 'Config::Neat::Array';
186              
187 65         462 my @a = @{$node->{'@inherit'}};
  65         175  
188              
189 65         510 my $intermediate = new_ixhash;
190              
191 65         170 foreach my $from (@a) {
192 70         285 my ($filename, $selector) = split('#', $from, 2);
193             # allow .#selector style to indicate the current file, since #selector
194             # without the leading symbol will be treated as a comment line
195 70 100       218 $filename = '' if $filename eq '.';
196 70 50 66     272 die "Neither filename nor selector are specified" unless $filename or $selector;
197              
198             # normalize path and selector
199 70 100       230 my $fullpath = $filename eq '' ? $self->{fullpath} : rel2abs($filename, $dir); # make path absolute based on current context dir
200 70         766 $selector =~ s/^\///; # remove leading slash, if any
201              
202 70         189 $from = $fullpath.'#'.$selector;
203              
204             # make sure we don't have any infinite loops
205             map {
206 19 100       126 die "Infinite loop detected at `\@inherit $from`" if $from eq $_;
207 70         119 } @{$self->{include_stack}};
  70         172  
208              
209 67         105 push @{$self->{include_stack}}, $from;
  67         164  
210              
211 67         122 my $merge_node;
212 67 100       171 if (exists $self->{cache}->{$from}) {
213 8         27 $merge_node = _clone($self->{cache}->{$from});
214             } else {
215 59         108 my $merge_cfg;
216 59         114 my $merge_dir = $dir;
217 59 100       134 if ($filename) {
218 31         752 $merge_dir = dirname($fullpath);
219              
220 31 50       112 if (!exists $self->{cache}->{$fullpath}) {
221 31         128 $self->{cache}->{$fullpath} = $self->_parse_file($fullpath);
222             }
223 29         96 $merge_cfg = _clone($self->{cache}->{$fullpath});
224             } else {
225 28         97 $merge_cfg = _clone($base_node);
226             }
227              
228 57         250 $merge_node = $self->select_subnode($merge_cfg, $selector, $dir);
229 56         225 $merge_node = $self->expand_data($base_node, $merge_node, $merge_dir);
230 53         140 $self->{cache}->{$from} = _clone($merge_node);
231             }
232              
233 61         264 $intermediate = $self->merge_data($merge_node, $intermediate, $dir);
234 61         167 pop @{$self->{include_stack}};
  61         239  
235             }
236              
237 56         190 delete $node->{'@inherit'};
238              
239 56         1438 $node = $self->merge_data($node, $intermediate, $dir);
240             }
241             }
242              
243 170         788 return $node;
244             }
245              
246             sub select_subnode {
247 57     57 0 162 my ($self, $node, $selector, $dir) = @_;
248              
249 57 50       197 die "Bad selector syntax (double slash) in '$selector'" if $selector =~ m/\/{2,}/;
250 57 50       167 die "Bad selector syntax (leading slash) in '$selector'" if $selector =~ m/^\//;
251              
252 57 100       169 return _clone($node) if $selector eq '';
253              
254 34         121 my @a = split('/', $selector);
255              
256 34         70 my $result = $node;
257 34         82 foreach (@a) {
258 53 50       270 next if ($_ eq '');
259 53 100 33     148 if (is_hash($result) && exists $result->{$_}) {
260 52         325 $result = $result->{$_};
261             } else {
262 1         18 die "Can't find key '$_' in node (selector: '$selector')";
263             }
264             }
265 33         264 return _clone($result);
266             }
267              
268             sub _clone {
269 266     266   481 my $data = shift;
270 266 50       13508 return ref($data) ? dclone($data) : $data;
271             }
272              
273             # merge tree structure from data2 into data1
274             # data1 is the one that may contain `-key` and `+key` entries
275             sub merge_data {
276 226     226 0 582 my ($self, $data1, $data2, $dir) = @_;
277              
278 226 100 100     1351 if (is_hash($data1) && is_hash($data2)) {
    100 100        
279 128         377 my @keys = get_keys_in_order($data2, $data1);
280              
281 128         2137 foreach my $key (keys %$data1) {
282 120 100       1712 my $base_key = $key =~ m/^[\+\-](.*)$/ ? $1 : $key;
283              
284 120 100       334 if ($key =~ m/^-(.*)$/) {
285 11 50       38 die "Key '$key' contains bogus data; expected an empty or true value" unless $data1->{$key}->as_boolean;
286 11         49 delete $data1->{$key};
287 11         235 delete $data2->{$1};
288 11         191 next;
289             }
290              
291             # arrays are NOT merged by default; use `+key` to merge arrays
292 109 100       395 if (is_neat_array($data1->{$key})) {
293 73 100       197 if ($key =~ m/^\+(.*)$/) {
294 13 50 66     47 if ((!exists $data2->{$base_key} || is_neat_array($data2->{$base_key}))) {
295 13         60 $data1 = rename_ixhash_key($data1, $key, $base_key);
296 13         33 $key = $base_key;
297             }
298              
299 13 100 66     62 if (is_simple_array($data1->{$key}) && exists $data2->{$base_key}) {
300             # we are about to merge two arrays; before doing so,
301             # if the accumulator array is a simple one, we need to convert it
302             # into an array, containing a single array ref, to maintain the
303             # 'array of arrays' structure
304 8         53 $data1->{$key} = Config::Neat::Array->new([$data1->{$key}]);
305             }
306             } else {
307 60         193 delete $data2->{$key};
308             }
309             }
310              
311             # hashes are merged by default; `+key { }` is the same as `key { }`
312 109 100 100     895 if (is_hash($data1->{$key}) && ($key =~ m/^\+(.*)$/)) {
313 5         23 $data1 = rename_ixhash_key($data1, $key, $base_key);
314 5         12 $key = $base_key;
315             }
316              
317 109 100 100     341 if (is_hash($data1->{$key}) && is_hash($data2->{$key})) {
318 11         45 my $offset = get_next_auto_key($data2->{$key});
319 11         48 $data1->{$key} = offset_keys($data1->{$key}, $offset);
320             }
321 109         500 $data1->{$key} = $self->merge_data($data1->{$key}, $data2->{$key}, $dir);
322             }
323              
324 128         1512 foreach my $key (keys %$data2) {
325 63 100 66     841 if (exists $data2->{$key} && !exists $data1->{$key}) {
326 42         480 $data1->{$key} = $data2->{$key};
327             }
328             }
329              
330 128 50       1641 $data1 = to_ixhash($data1) unless is_ixhash($data1);
331 128         369 $data1 = reorder_ixhash($data1, \@keys);
332             } elsif (is_neat_array($data1) && is_neat_array($data2)) {
333             # always push data as array ref, not as individual items
334             # to maintain 'array of arrays' structure
335 10 100       31 if (is_simple_array($data2)) {
336 8         22 unshift(@$data1, $data2);
337             } else {
338 2         9 unshift(@$data1, @$data2);
339             }
340             }
341              
342 226         822 return $data1;
343             }
344              
345             1;