File Coverage

lib/Config/Neat/Render.pm
Criterion Covered Total %
statement 142 147 96.6
branch 74 90 82.2
condition 36 51 70.5
subroutine 13 13 100.0
pod 2 9 22.2
total 267 310 86.1


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.302';
180              
181 2     2   1855 use strict;
  2         4  
  2         54  
182              
183 2     2   15 no warnings qw(uninitialized);
  2         7  
  2         75  
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   10 hash_has_sequential_keys);
  2         4  
188 2     2   10 use Tie::IxHash;
  2         3  
  2         2590  
189              
190             my $PARAM = 1;
191             my $BLOCK = 2;
192              
193             #
194             # Initialize object
195             #
196             sub new {
197 2     2 1 599 my ($class, $options) = @_;
198              
199 2         10 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       7 $options = {} unless $options;
216 2         13 %$options = (%$default_options, %$options);
217              
218 2         5 my $self = {
219             _options => $options
220             };
221              
222 2         4 bless $self, $class;
223 2         7 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 15111 my ($self, $data, $options) = @_;
249              
250 38 100       138 $options = {} unless $options;
251 38         82 %$options = (%{$self->{_options}}, %$options);
  38         389  
252              
253 38         114 $options->{global_key_length} = 0;
254              
255             # convert an array into a hash with 0..n values
256 38         82 my $sort = $options->{sort};
257 38 100       130 if (ref($sort) eq 'ARRAY') {
258 1         1 my %h;
259 1         6 @h{@$sort} = (0 .. scalar(@$sort) - 1);
260 1         3 $options->{sort} = \%h;
261             }
262              
263             sub max_key_length {
264 262     262 0 500 my ($node, $options, $indent, $recursive) = @_;
265              
266 262         423 my $len = 0;
267 262 100 66     583 if (is_hash($node)) {
    100 66        
268 140         398 foreach my $key (keys %$node) {
269 216         2334 my $subnode = $node->{$key};
270              
271 216 100 100     1428 if (is_array($subnode) && !is_homogenous_simple_array($subnode)) {
272 1         3 $subnode = convert_array_to_hash($subnode);
273             }
274              
275 216         346 my $key_len;
276 216 100 100     463 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 123         217 $key_len = $indent + length($key);
281 123 100       274 $len = $key_len if $key_len > $len;
282             }
283              
284 216 100 100     1034 if ($recursive && (is_hash($subnode) || is_neat_array($subnode) || is_array($subnode))) {
      33        
285 206 100       429 my $sub_indent = is_hash($subnode) ? $options->{indentation} : 0;
286 206         486 my $child_len = max_key_length($subnode, $options, $indent + $sub_indent, $recursive);
287 206         303 my $key_len = $child_len;
288 206 100       511 $len = $key_len if $key_len > $len;
289             }
290             }
291             } elsif ((is_neat_array($node) || is_array($node)) && !is_homogenous_simple_array($node)) {
292             map {
293 8         22 my $child_len = max_key_length($_, $options, $indent + $options->{indentation}, $recursive);
  18         49  
294 18         24 my $key_len = $child_len;
295 18 100       57 $len = $key_len if $key_len > $len;
296             } @$node;
297             }
298 262         526 return $len;
299             }
300              
301             sub convert_array_to_hash {
302 2     2 0 4 my $node = shift;
303              
304 2         2 my $i = 0;
305              
306 2         6 my $h = new_ixhash;
307              
308 2         4 foreach my $value (@$node) {
309 6         81 $h->{$i++} = $value;
310             }
311 2         24 return $h;
312             }
313              
314             sub render_wrapped_array {
315 124     124 0 246 my ($array, $options, $indent) = @_;
316              
317 124         260 my $wrap_width = $options->{wrap_width};
318              
319 124         181 my @a;
320 124         197 my $line = '';
321 124         232 foreach my $item (@$array) {
322 246 100       498 my $l = $line ? length($line) + 1 : 0;
323              
324 246 100       511 if ($l + length($item) > $wrap_width) {
325 10 100       26 push(@a, $line) if $line ne '';
326 10         16 $line = '';
327             }
328              
329 246 100       434 if (length($item) >= $wrap_width) {
330 2         4 push(@a, $item);
331             } else {
332 244 100       510 $line .= ' ' if $line ne '';
333 244         403 $line .= $item;
334             }
335             }
336 124 50       338 push(@a, $line) if $line ne '';
337              
338 124         489 return join("\n".(' ' x $indent), @a);
339             }
340              
341             sub render_scalar {
342 221     221 0 408 my ($scalar, $options, $indent, $should_escape) = @_;
343              
344             # dereference scalar
345 221 50       490 $scalar = $$scalar if ref($scalar) eq 'SCALAR';
346              
347 221         485 $scalar =~ s/`/\\`/g;
348              
349 221 50       851 if ($scalar =~ m/(\n|\s{2,})/) {
350 0         0 $should_escape = 1;
351             }
352              
353 221 100       474 if (!defined $scalar) {
354 1         2 $scalar = $options->{undefined_value};
355             }
356              
357 221 100       487 if ($scalar eq '') {
358 8         10 $scalar = '``';
359             }
360              
361 221 100 100     838 if ($should_escape and $scalar =~ m/\s/) {
362 8         21 $scalar = '`'.$scalar.'`';
363             }
364              
365 221 100 66     523 if (!$should_escape and $scalar ne '') {
366 10         39 my @a = split(/\s+/, $scalar);
367 10         22 return render_wrapped_array(\@a, $options, $indent);
368             }
369              
370 211         559 return $scalar;
371             }
372              
373             sub pad {
374 124     124 0 228 my ($s, $width) = @_;
375 124         221 my $spaces = $width - length($s);
376 124 100       607 return ($spaces <= 0) ? $s : $s . ' ' x $spaces;
377             }
378              
379             sub render_key_val {
380 234     234 0 688 my ($options, $key_length, $indent, $wasref, $array_mode, $sequential_keys, $key, $val) = @_;
381              
382 234         1311 my $text = '';
383 234         449 my $space_indent = (' ' x $indent);
384              
385 234 50       690 die "Keys should not conain whitespace" if ($key =~ m/\s/);
386              
387 234 100       604 if (is_scalar($val)) {
    100          
    100          
388 10 0 33     25 $text .= "\n" if ($$wasref == $BLOCK) and $options->{separate_blocks};
389              
390             $text .= $space_indent .
391             pad($key, $key_length - $indent) .
392             (' ' x $options->{key_spacing}) .
393 10         19 render_scalar($val, $options, $key_length + $options->{key_spacing}) .
394             "\n";
395              
396 10         20 $$wasref = $PARAM;
397              
398             } elsif (is_homogenous_simple_array($val)) {
399             # escape individual array items
400 114         239 my @a = map { render_scalar($_, $options, undef, 1) } @$val;
  211         420  
401              
402 114 50 66     641 $text .= "\n" if ($$wasref == $BLOCK) and $options->{separate_blocks};
403              
404             $text .= $space_indent .
405             pad($key, $key_length - $indent) .
406             (' ' x $options->{key_spacing}) .
407 114         298 render_wrapped_array(\@a, $options, $key_length + $options->{key_spacing}) .
408             "\n";
409              
410 114         275 $$wasref = $PARAM;
411              
412             } elsif (is_neat_array($val)) {
413             map {
414 8         20 $text .= render_key_val($options, $key_length, $indent, $wasref, $array_mode, $sequential_keys, $key, $_);
  18         67  
415             } @$val;
416              
417             } else {
418 102 50 66     325 $text .= "\n" if $$wasref and $options->{separate_blocks};
419              
420 102 100 66     244 if (is_hash($val) && exists $val->{''}) {
421 5         32 my $default_value = $val->{''};
422 5 50 33     37 if (!is_scalar($default_value) && !is_homogenous_simple_array($default_value)) {
423 0         0 die "Only scalar or simple array can be rendered as a default node value";
424             }
425 5         11 $$wasref = $PARAM;
426 5         18 $text .= render_key_val($options, $key_length, $indent, $wasref, undef, $sequential_keys, $key, $default_value);
427 5         13 $text .= $space_indent;
428             } else {
429 97         440 $text .= $space_indent;
430              
431 97 100 66     390 if (!$array_mode && !($sequential_keys && is_number($key))) {
      66        
432 78 50       257 $text .= $options->{brace_under} ? "$key\n$space_indent" : "$key ";
433             }
434             }
435              
436             $text .= "{\n" .
437 102         348 render_node_recursively($val, $options, $indent + $options->{indentation}) .
438             $space_indent .
439             "}\n";
440              
441 102         201 $$wasref = $BLOCK;
442             }
443              
444 234         700 return $text;
445             }
446              
447             sub render_node_recursively {
448 140     140 0 280 my ($node, $options, $indent) = @_;
449 140         256 my $text = '';
450 140         207 my $key_length = 0;
451 140         233 my $array_mode;
452             my $sequential_keys;
453              
454 140 100 66     302 if (is_array($node) || is_neat_array($node)) {
455 1 50       2 if (is_homogenous_simple_array($node)) {
456 0         0 die "Can't render simple arrays as a main block content";
457             } else {
458 1         2 $array_mode = 1;
459 1         3 $node = convert_array_to_hash($node);
460             }
461             }
462              
463 140 50       338 if (is_hash($node)) {
464 140         327 $array_mode = hash_has_only_sequential_keys($node);
465 140         378 $sequential_keys = hash_has_sequential_keys($node);
466 140 50       344 $key_length = $options->{align_all} ? $options->{global_key_length} : max_key_length($node, $options, $indent);
467              
468             } else {
469 0         0 die "Unsupported data type: '".ref($node)."'";
470             }
471              
472 140         223 my $was = undef;
473              
474 140         224 my $sort = $options->{sort};
475 140         342 my @keys = keys %$node;
476 140 100 100     2234 if (!$array_mode and scalar(@keys) > 1) {
477 39 100       95 if (is_hash($sort)) {
    50          
478 8         23 @keys = sort { $sort->{$a} <=> $sort->{$b} } @keys;
  36         62  
479             } elsif ($sort) {
480 0         0 @keys = sort @keys;
481             }
482             }
483              
484 140         273 foreach my $key (@keys) {
485             # default node values are rendered separately
486 216 100       567 if ($key ne '') {
487 211         720 $text .= render_key_val($options, $key_length, $indent, \$was, $array_mode, $sequential_keys, $key, $node->{$key});
488             }
489             }
490 140         503 return $text;
491             }
492              
493 38 50       111 if ($options->{align_all}) {
494             # calculate indent recursively
495 38         146 $options->{global_key_length} = max_key_length($data, $options, 0, 1);
496             }
497              
498 38         120 return render_node_recursively($data, $options, 0);
499             }
500              
501             1;