File Coverage

lib/Config/Neat/Render.pm
Criterion Covered Total %
statement 145 150 96.6
branch 77 92 83.7
condition 39 51 76.4
subroutine 13 13 100.0
pod 2 9 22.2
total 276 315 87.6


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Config::Neat::Render - Render configs in Config::Neat format
4              
5             =head1 SYNOPSIS
6              
7             use Config::Neat::Render;
8              
9             my $r = Config::Neat::Render->new();
10              
11             my $data = {
12             'foo' => 'Hello, World!',
13             'bar' => [1, 2, 3],
14             'baz' => {
15             'etc' => ['foo bar', 'baz', '', 1]
16             }
17             };
18              
19             print $r->render($data);
20              
21             The output will be:
22              
23             bar 1 2 3
24              
25             baz
26             {
27             etc `foo bar` baz `` 1
28             }
29              
30             foo Hello, World!
31              
32             =head1 DESCRIPTION
33              
34             This module allows you to render Config::Neat-compatible structures from your data
35             (but read below for limitations). See
36             L
37             for the detailed file syntax specification. For parsing, use L.
38              
39             =head2 METHODS
40              
41             =over 4
42              
43             =item B<< Config::Neat::Render->new([$options]) >>
44              
45             Constructs a new renderer object. $options is a reference to a hash containing
46             rendering options' overrides (see the RENDERING OPTIONS section below).
47              
48             =item B<< Config::Neat::Render->render($data[, $options]) >>
49              
50             Renders $data into a string and returns it. $options is a reference to a hash
51             containing rendering options' overrides (see the RENDERING OPTIONS section below).
52              
53             =back
54              
55             =head2 RENDERING OPTIONS
56              
57             =over 4
58              
59             =item B<< indentation >>
60              
61             A number of spaces to indent each nested block contents with.
62              
63             Default value: C<4>
64              
65             =item B<< key_spacing >>
66              
67             A number of spaces between a key and and a value.
68              
69             Default value: C<4>
70              
71             =item B<< wrap_width >>
72              
73             A suggested maximum width of each line in a multiline string or array.
74              
75             Default value: C<60>
76              
77             =item B<< brace_under >>
78              
79             If true, put the opening brace under the key name, not on the same line
80              
81             Default value: C<1> (true)
82              
83             =item B<< separate_blocks >>
84              
85             If true, surrond blocks with empty lines for better readability.
86              
87             Default value: C<1> (true)
88              
89             =item B<< align_all >>
90              
91             If true, align all values in the configuration file
92             (otherwise the values are aligned only within current block).
93              
94             Default value: C<1> (true)
95              
96             =item B<< sort >>
97              
98             Note that hashes in Perl do not guarantee the correct order, so blocks may have
99             individual parameters shuffled randomly. Set this option to a true value
100             if you want to sort keys alphabetically, or to a reference to an array holding
101             an ordered list of key names
102              
103             Default value: C (false)
104              
105             Example:
106              
107             my $data = {
108             'bar' => [1, 2, 3],
109             'baz' => {
110             'etc' => ['foo bar', 'baz', '', 1]
111             }
112             'foo' => 'Hello, World!',
113             };
114              
115             my @order = qw(foo bar baz);
116              
117             print $r->render($data, {sort => \@order});
118              
119             The output will be:
120              
121             foo Hello, World!
122             bar 1 2 3
123              
124             baz
125             {
126             etc `foo bar` baz `` 1
127             }
128              
129             =item B<< undefined_value >>
130              
131             A string representation of the value to emit for undefined values
132              
133             Default value: C<'NO'>
134              
135             =back
136              
137             =head1 LIMITATIONS
138              
139             Do not use L in conjunction with L for
140             arbitrary data serialization/desrialization. JSON and YAML will work better
141             for this kind of task.
142              
143             Why? Because Config::Neat was primarily designed to allow easier configuration
144             file authoring and reading, and uses relaxed syntax where strings are treated like
145             space-separated arrays (and vice versa), and where there's no strict definition
146             for boolean types, no null values, etc.
147              
148             It's the developer's responsibility to treat any given parameter as a boolean,
149             or string, or an array. This means that once you serialize your string into
150             Config::Neat format and parse it back, it will be converted to an array,
151             and you will need to use `->as_string` method to get the value as string.
152              
153             In other words, when doing this:
154              
155             my $c = Config::Neat->new();
156             my $r = Config::Neat::Render->new();
157             my $parsed_data = $c->parse($r->render($arbitrary_data));
158              
159             $parsed_data will almost always be different from $arbitrary_data.
160              
161             However, doing this immediately after:
162              
163             my $parsed_data_2 = $c->parse($r->render($parsed_data));
164              
165             Should produce the same data structure again.
166              
167             =head1 COPYRIGHT
168              
169             Copyright (C) 2012-2015 Igor Afanasyev
170              
171             =head1 SEE ALSO
172              
173             L
174              
175             =cut
176              
177             package Config::Neat::Render;
178              
179             our $VERSION = '1.4';
180              
181 2     2   2049 use strict;
  2         3  
  2         53  
182              
183 2     2   8 no warnings qw(uninitialized);
  2         4  
  2         65  
184              
185 2         121 use Config::Neat::Util qw(new_ixhash is_number is_code is_hash is_array is_scalar
186             is_neat_array is_homogenous_simple_array hash_has_only_sequential_keys
187 2     2   9 hash_has_sequential_keys);
  2         3  
188 2     2   10 use Tie::IxHash;
  2         3  
  2         3032  
189              
190             my $PARAM = 1;
191             my $BLOCK = 2;
192              
193             #
194             # Initialize object
195             #
196             sub new {
197 2     2 1 499 my ($class, $options) = @_;
198              
199 2         9 my $default_options = {
200             indentation => 4, # number of spaces to indent each nested block contents with
201             key_spacing => 4, # number of spaces between a key and and a value
202              
203             wrap_width => 60, # a suggested maximum width of each line in a multiline string or array
204              
205             brace_under => 1, # if true, put the opening brace under the key name, not on the same line
206             separate_blocks => 1, # if true, surrond blocks with empty lines for better readability
207             align_all => 1, # if true, align all values in the configuration file
208             # (otherwise the values are aligned only within current block)
209              
210             sort => undef, # can be a true value if you want to sort keys alphabetically
211             # or a reference to an array with an ordered list of key names
212             undefined_value => 'NO' # default value to emit for undefined values
213             };
214              
215 2 50       8 $options = {} unless $options;
216 2         10 %$options = (%$default_options, %$options);
217              
218 2         5 my $self = {
219             _options => $options
220             };
221              
222 2         4 bless $self, $class;
223 2         6 return $self;
224             }
225              
226             # Renders a nested tree structure into a Config::Neat-compatible text representaion.
227             # @@@@@@@@
228             # CAUTION: Config::Neat::Render->render() and Config::Neat->parse()
229             # are NOT SYMMETRICAL and should not be used for arbitrary data
230             # serialization/deserialization.
231             #
232             # In other words, when doing this:
233             #
234             # my $c = Config::Neat->new();
235             # my $r = Config::Neat::Render->new();
236             # my $parsed_data = $c->parse($r->render($arbitrary_data));
237             #
238             # $parsed_data will almost always be different from $arbitrary_data.
239             # However, doing this immediately after:
240             #
241             # my $parsed_data_2 = $c->parse($r->render($parsed_data));
242             #
243             # Should produce the same data structure again.
244             #
245             # See README for more information.
246             # @@@@@@@@
247             sub render {
248 38     38 1 12631 my ($self, $data, $options) = @_;
249              
250 38 100       108 $options = {} unless $options;
251 38         70 %$options = (%{$self->{_options}}, %$options);
  38         267  
252              
253 38         78 $options->{global_key_length} = 0;
254              
255             # convert an array into a hash with 0..n values
256 38         53 my $sort = $options->{sort};
257 38 100       81 if (ref($sort) eq 'ARRAY') {
258 1         2 my %h;
259 1         6 @h{@$sort} = (0 .. scalar(@$sort) - 1);
260 1         2 $options->{sort} = \%h;
261             }
262              
263             sub max_key_length {
264 263     263 0 394 my ($node, $options, $indent, $recursive) = @_;
265              
266 263         281 my $len = 0;
267 263 100 66     378 if (is_hash($node)) {
    100 66        
268 140         316 foreach my $key (keys %$node) {
269 218         1980 my $subnode = $node->{$key};
270              
271 218 100 100     1124 if (is_array($subnode) && !is_homogenous_simple_array($subnode)) {
272 1         3 $subnode = convert_array_to_hash($subnode);
273             }
274              
275 218         263 my $key_len;
276 218 100 100     290 if (is_hash($subnode) && !exists $subnode->{''}) {
277             # do not take into account the length of a hash key
278             # if it doesn't contain default values (which we want to align as well)
279             } else {
280 125         170 $key_len = $indent + length($key);
281             # if the key contains spaces and will be wrapped
282             # with `...`, add two extra symbols
283 125 100       293 if ($key =~ m/\s/) {
284 2         4 $key_len += 2;
285             }
286 125 100       238 $len = $key_len if $key_len > $len;
287             }
288              
289 218 100 100     766 if ($recursive && (is_hash($subnode) || is_neat_array($subnode) || is_array($subnode))) {
      66        
290 207 100       297 my $sub_indent = is_hash($subnode) ? $options->{indentation} : 0;
291 207         352 my $child_len = max_key_length($subnode, $options, $indent + $sub_indent, $recursive);
292 207         226 my $key_len = $child_len;
293 207 100       400 $len = $key_len if $key_len > $len;
294             }
295             }
296             } elsif ((is_neat_array($node) || is_array($node)) && !is_homogenous_simple_array($node)) {
297             map {
298 8         18 my $child_len = max_key_length($_, $options, $indent + $options->{indentation}, $recursive);
  18         34  
299 18         24 my $key_len = $child_len;
300 18 100       45 $len = $key_len if $key_len > $len;
301             } @$node;
302             }
303 263         425 return $len;
304             }
305              
306             sub convert_array_to_hash {
307 2     2 0 2 my $node = shift;
308              
309 2         2 my $i = 0;
310              
311 2         4 my $h = new_ixhash;
312              
313 2         4 foreach my $value (@$node) {
314 6         70 $h->{$i++} = $value;
315             }
316 2         21 return $h;
317             }
318              
319             sub render_wrapped_array {
320 126     126 0 183 my ($array, $options, $indent) = @_;
321              
322 126         154 my $wrap_width = $options->{wrap_width};
323              
324 126         130 my @a;
325 126         151 my $line = '';
326 126         173 foreach my $item (@$array) {
327 250 100       309 my $l = $line ? length($line) + 1 : 0;
328              
329 250 100       364 if ($l + length($item) > $wrap_width) {
330 10 100       19 push(@a, $line) if $line ne '';
331 10         11 $line = '';
332             }
333              
334 250 100       321 if (length($item) >= $wrap_width) {
335 2         4 push(@a, $item);
336             } else {
337 248 100       350 $line .= ' ' if $line ne '';
338 248         340 $line .= $item;
339             }
340             }
341 126 50       266 push(@a, $line) if $line ne '';
342              
343 126         425 return join("\n".(' ' x $indent), @a);
344             }
345              
346             sub render_scalar {
347 224     224 0 341 my ($scalar, $options, $indent, $should_escape) = @_;
348              
349             # dereference scalar
350 224 50       325 $scalar = $$scalar if ref($scalar) eq 'SCALAR';
351              
352 224         331 $scalar =~ s/`/\\`/g;
353              
354 224 50       658 if ($scalar =~ m/(\n|\s{2,})/) {
355 0         0 $should_escape = 1;
356             }
357              
358 224 100       324 if (!defined $scalar) {
359 1         2 $scalar = $options->{undefined_value};
360             }
361              
362 224 100       342 if ($scalar eq '') {
363 8         9 $scalar = '``';
364             }
365              
366 224 100 100     564 if ($should_escape and $scalar =~ m/\s/) {
367 8         14 $scalar = '`'.$scalar.'`';
368             }
369              
370 224 100 66     369 if (!$should_escape and $scalar ne '') {
371 11         37 my @a = split(/\s+/, $scalar);
372 11         15 return render_wrapped_array(\@a, $options, $indent);
373             }
374              
375 213         441 return $scalar;
376             }
377              
378             sub pad {
379 126     126 0 179 my ($s, $width) = @_;
380 126         162 my $spaces = $width - length($s);
381 126 100       491 return ($spaces <= 0) ? $s : $s . ' ' x $spaces;
382             }
383              
384             sub render_key_val {
385 236     236 0 591 my ($options, $key_length, $indent, $wasref, $array_mode, $sequential_keys, $key, $val) = @_;
386              
387 236         1051 my $text = '';
388 236         357 my $space_indent = (' ' x $indent);
389              
390             # if the key name contains whitespace, wrap it in backticks
391 236 100       503 if ($key =~ m/\s/) {
392 2         5 $key = "`$key`";
393             }
394              
395 236 100       398 if (is_scalar($val)) {
    100          
    100          
396 11 0 33     23 $text .= "\n" if ($$wasref == $BLOCK) and $options->{separate_blocks};
397              
398             $text .= $space_indent .
399             pad($key, $key_length - $indent) .
400             (' ' x $options->{key_spacing}) .
401 11         19 render_scalar($val, $options, $key_length + $options->{key_spacing}) .
402             "\n";
403              
404 11         23 $$wasref = $PARAM;
405              
406             } elsif (is_homogenous_simple_array($val)) {
407             # escape individual array items
408 115         199 my @a = map { render_scalar($_, $options, undef, 1) } @$val;
  213         320  
409              
410 115 50 66     243 $text .= "\n" if ($$wasref == $BLOCK) and $options->{separate_blocks};
411              
412             $text .= $space_indent .
413             pad($key, $key_length - $indent) .
414             (' ' x $options->{key_spacing}) .
415 115         197 render_wrapped_array(\@a, $options, $key_length + $options->{key_spacing}) .
416             "\n";
417              
418 115         205 $$wasref = $PARAM;
419              
420             } elsif (is_neat_array($val)) {
421             map {
422 8         16 $text .= render_key_val($options, $key_length, $indent, $wasref, $array_mode, $sequential_keys, $key, $_);
  18         34  
423             } @$val;
424              
425             } else {
426 102 50 66     257 $text .= "\n" if $$wasref and $options->{separate_blocks};
427              
428 102 100 100     183 if (is_hash($val) && exists $val->{''}) {
429 5         28 my $default_value = $val->{''};
430 5 50 33     32 if (!is_scalar($default_value) && !is_homogenous_simple_array($default_value)) {
431 0         0 die "Only scalar or simple array can be rendered as a default node value";
432             }
433 5         10 $$wasref = $PARAM;
434 5         36 $text .= render_key_val($options, $key_length, $indent, $wasref, undef, $sequential_keys, $key, $default_value);
435 5         9 $text .= $space_indent;
436             } else {
437 97         393 $text .= $space_indent;
438              
439 97 100 66     236 if (!$array_mode && !($sequential_keys && is_number($key))) {
      100        
440 78 50       187 $text .= $options->{brace_under} ? "$key\n$space_indent" : "$key ";
441             }
442             }
443              
444             $text .= "{\n" .
445 102         257 render_node_recursively($val, $options, $indent + $options->{indentation}) .
446             $space_indent .
447             "}\n";
448              
449 102         145 $$wasref = $BLOCK;
450             }
451              
452 236         618 return $text;
453             }
454              
455             sub render_node_recursively {
456 140     140 0 228 my ($node, $options, $indent) = @_;
457 140         167 my $text = '';
458 140         150 my $key_length = 0;
459 140         179 my $array_mode;
460             my $sequential_keys;
461              
462 140 100 66     213 if (is_array($node) || is_neat_array($node)) {
463 1 50       3 if (is_homogenous_simple_array($node)) {
464 0         0 die "Can't render simple arrays as a main block content";
465             } else {
466 1         2 $array_mode = 1;
467 1         2 $node = convert_array_to_hash($node);
468             }
469             }
470              
471 140 50       229 if (is_hash($node)) {
472 140         223 $array_mode = hash_has_only_sequential_keys($node);
473 140         255 $sequential_keys = hash_has_sequential_keys($node);
474 140 50       260 $key_length = $options->{align_all} ? $options->{global_key_length} : max_key_length($node, $options, $indent);
475              
476             } else {
477 0         0 die "Unsupported data type: '".ref($node)."'";
478             }
479              
480 140         168 my $was = undef;
481              
482 140         162 my $sort = $options->{sort};
483 140         270 my @keys = keys %$node;
484 140 100 100     1732 if (!$array_mode and scalar(@keys) > 1) {
485 39 100       61 if (is_hash($sort)) {
    50          
486 8         24 @keys = sort { $sort->{$a} <=> $sort->{$b} } @keys;
  46         64  
487             } elsif ($sort) {
488 0         0 @keys = sort @keys;
489             }
490             }
491              
492 140         222 foreach my $key (@keys) {
493             # default node values are rendered separately
494 218 100       360 if ($key ne '') {
495 213         535 $text .= render_key_val($options, $key_length, $indent, \$was, $array_mode, $sequential_keys, $key, $node->{$key});
496             }
497             }
498 140         388 return $text;
499             }
500              
501 38 50       80 if ($options->{align_all}) {
502             # calculate indent recursively
503 38         84 $options->{global_key_length} = max_key_length($data, $options, 0, 1);
504             }
505              
506 38         84 return render_node_recursively($data, $options, 0);
507             }
508              
509             1;