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.401';
180              
181 2     2   2470 use strict;
  2         4  
  2         64  
182              
183 2     2   11 no warnings qw(uninitialized);
  2         3  
  2         86  
184              
185 2         148 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   12 hash_has_sequential_keys);
  2         9  
188 2     2   13 use Tie::IxHash;
  2         6  
  2         3822  
189              
190             my $PARAM = 1;
191             my $BLOCK = 2;
192              
193             #
194             # Initialize object
195             #
196             sub new {
197 2     2 1 679 my ($class, $options) = @_;
198              
199 2         12 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       9 $options = {} unless $options;
216 2         13 %$options = (%$default_options, %$options);
217              
218 2         5 my $self = {
219             _options => $options
220             };
221              
222 2         5 bless $self, $class;
223 2         8 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 14492 my ($self, $data, $options) = @_;
249              
250 38 100       116 $options = {} unless $options;
251 38         56 %$options = (%{$self->{_options}}, %$options);
  38         297  
252              
253 38         99 $options->{global_key_length} = 0;
254              
255             # convert an array into a hash with 0..n values
256 38         69 my $sort = $options->{sort};
257 38 100       95 if (ref($sort) eq 'ARRAY') {
258 1         2 my %h;
259 1         8 @h{@$sort} = (0 .. scalar(@$sort) - 1);
260 1         3 $options->{sort} = \%h;
261             }
262              
263             sub max_key_length {
264 263     263 0 474 my ($node, $options, $indent, $recursive) = @_;
265              
266 263         347 my $len = 0;
267 263 100 66     455 if (is_hash($node)) {
    100 66        
268 140         386 foreach my $key (keys %$node) {
269 218         2332 my $subnode = $node->{$key};
270              
271 218 100 100     1311 if (is_array($subnode) && !is_homogenous_simple_array($subnode)) {
272 1         19 $subnode = convert_array_to_hash($subnode);
273             }
274              
275 218         328 my $key_len;
276 218 100 100     362 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         215 $key_len = $indent + length($key);
281             # if the key contains spaces and will be wrapped
282             # with `...`, add two extra symbols
283 125 100       345 if ($key =~ m/\s/) {
284 2         4 $key_len += 2;
285             }
286 125 100       258 $len = $key_len if $key_len > $len;
287             }
288              
289 218 100 100     920 if ($recursive && (is_hash($subnode) || is_neat_array($subnode) || is_array($subnode))) {
      66        
290 207 100       366 my $sub_indent = is_hash($subnode) ? $options->{indentation} : 0;
291 207         433 my $child_len = max_key_length($subnode, $options, $indent + $sub_indent, $recursive);
292 207         284 my $key_len = $child_len;
293 207 100       462 $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         40  
299 18         29 my $key_len = $child_len;
300 18 100       51 $len = $key_len if $key_len > $len;
301             } @$node;
302             }
303 263         508 return $len;
304             }
305              
306             sub convert_array_to_hash {
307 2     2 0 5 my $node = shift;
308              
309 2         17 my $i = 0;
310              
311 2         6 my $h = new_ixhash;
312              
313 2         5 foreach my $value (@$node) {
314 6         80 $h->{$i++} = $value;
315             }
316 2         26 return $h;
317             }
318              
319             sub render_wrapped_array {
320 126     126 0 232 my ($array, $options, $indent) = @_;
321              
322 126         177 my $wrap_width = $options->{wrap_width};
323              
324 126         168 my @a;
325 126         178 my $line = '';
326 126         217 foreach my $item (@$array) {
327 250 100       413 my $l = $line ? length($line) + 1 : 0;
328              
329 250 100       459 if ($l + length($item) > $wrap_width) {
330 10 100       22 push(@a, $line) if $line ne '';
331 10         26 $line = '';
332             }
333              
334 250 100       391 if (length($item) >= $wrap_width) {
335 2         4 push(@a, $item);
336             } else {
337 248 100       410 $line .= ' ' if $line ne '';
338 248         401 $line .= $item;
339             }
340             }
341 126 50       313 push(@a, $line) if $line ne '';
342              
343 126         491 return join("\n".(' ' x $indent), @a);
344             }
345              
346             sub render_scalar {
347 224     224 0 424 my ($scalar, $options, $indent, $should_escape) = @_;
348              
349             # dereference scalar
350 224 50       403 $scalar = $$scalar if ref($scalar) eq 'SCALAR';
351              
352 224         384 $scalar =~ s/`/\\`/g;
353              
354 224 50       781 if ($scalar =~ m/(\n|\s{2,})/) {
355 0         0 $should_escape = 1;
356             }
357              
358 224 100       448 if (!defined $scalar) {
359 1         2 $scalar = $options->{undefined_value};
360             }
361              
362 224 100       380 if ($scalar eq '') {
363 8         12 $scalar = '``';
364             }
365              
366 224 100 100     664 if ($should_escape and $scalar =~ m/\s/) {
367 8         19 $scalar = '`'.$scalar.'`';
368             }
369              
370 224 100 66     448 if (!$should_escape and $scalar ne '') {
371 11         41 my @a = split(/\s+/, $scalar);
372 11         23 return render_wrapped_array(\@a, $options, $indent);
373             }
374              
375 213         546 return $scalar;
376             }
377              
378             sub pad {
379 126     126 0 238 my ($s, $width) = @_;
380 126         188 my $spaces = $width - length($s);
381 126 100       595 return ($spaces <= 0) ? $s : $s . ' ' x $spaces;
382             }
383              
384             sub render_key_val {
385 236     236 0 674 my ($options, $key_length, $indent, $wasref, $array_mode, $sequential_keys, $key, $val) = @_;
386              
387 236         1273 my $text = '';
388 236         423 my $space_indent = (' ' x $indent);
389              
390             # if the key name contains whitespace, wrap it in backticks
391 236 100       579 if ($key =~ m/\s/) {
392 2         5 $key = "`$key`";
393             }
394              
395 236 100       517 if (is_scalar($val)) {
    100          
    100          
396 11 0 33     26 $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         22 render_scalar($val, $options, $key_length + $options->{key_spacing}) .
402             "\n";
403              
404 11         22 $$wasref = $PARAM;
405              
406             } elsif (is_homogenous_simple_array($val)) {
407             # escape individual array items
408 115         214 my @a = map { render_scalar($_, $options, undef, 1) } @$val;
  213         373  
409              
410 115 50 66     283 $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         241 render_wrapped_array(\@a, $options, $key_length + $options->{key_spacing}) .
416             "\n";
417              
418 115         254 $$wasref = $PARAM;
419              
420             } elsif (is_neat_array($val)) {
421             map {
422 8         19 $text .= render_key_val($options, $key_length, $indent, $wasref, $array_mode, $sequential_keys, $key, $_);
  18         41  
423             } @$val;
424              
425             } else {
426 102 50 66     352 $text .= "\n" if $$wasref and $options->{separate_blocks};
427              
428 102 100 100     222 if (is_hash($val) && exists $val->{''}) {
429 5         33 my $default_value = $val->{''};
430 5 50 33     39 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         12 $$wasref = $PARAM;
434 5         13 $text .= render_key_val($options, $key_length, $indent, $wasref, undef, $sequential_keys, $key, $default_value);
435 5         11 $text .= $space_indent;
436             } else {
437 97         471 $text .= $space_indent;
438              
439 97 100 66     336 if (!$array_mode && !($sequential_keys && is_number($key))) {
      100        
440 78 50       232 $text .= $options->{brace_under} ? "$key\n$space_indent" : "$key ";
441             }
442             }
443              
444             $text .= "{\n" .
445 102         301 render_node_recursively($val, $options, $indent + $options->{indentation}) .
446             $space_indent .
447             "}\n";
448              
449 102         183 $$wasref = $BLOCK;
450             }
451              
452 236         712 return $text;
453             }
454              
455             sub render_node_recursively {
456 140     140 0 333 my ($node, $options, $indent) = @_;
457 140         212 my $text = '';
458 140         173 my $key_length = 0;
459 140         202 my $array_mode;
460             my $sequential_keys;
461              
462 140 100 66     282 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         3 $node = convert_array_to_hash($node);
468             }
469             }
470              
471 140 50       297 if (is_hash($node)) {
472 140         288 $array_mode = hash_has_only_sequential_keys($node);
473 140         310 $sequential_keys = hash_has_sequential_keys($node);
474 140 50       315 $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         231 my $was = undef;
481              
482 140         202 my $sort = $options->{sort};
483 140         340 my @keys = keys %$node;
484 140 100 100     2163 if (!$array_mode and scalar(@keys) > 1) {
485 39 100       81 if (is_hash($sort)) {
    50          
486 8         28 @keys = sort { $sort->{$a} <=> $sort->{$b} } @keys;
  47         80  
487             } elsif ($sort) {
488 0         0 @keys = sort @keys;
489             }
490             }
491              
492 140         281 foreach my $key (@keys) {
493             # default node values are rendered separately
494 218 100       470 if ($key ne '') {
495 213         643 $text .= render_key_val($options, $key_length, $indent, \$was, $array_mode, $sequential_keys, $key, $node->{$key});
496             }
497             }
498 140         478 return $text;
499             }
500              
501 38 50       88 if ($options->{align_all}) {
502             # calculate indent recursively
503 38         87 $options->{global_key_length} = max_key_length($data, $options, 0, 1);
504             }
505              
506 38         107 return render_node_recursively($data, $options, 0);
507             }
508              
509             1;