File Coverage

blib/lib/Text/ZPL.pm
Criterion Covered Total %
statement 127 128 99.2
branch 55 58 94.8
condition 25 32 78.1
subroutine 16 16 100.0
pod 2 2 100.0
total 225 236 95.3


line stmt bran cond sub pod time code
1             package Text::ZPL;
2             $Text::ZPL::VERSION = '0.002002';
3 4     4   44118 use strict; use warnings FATAL => 'all';
  4     4   10  
  4         128  
  4         17  
  4         4  
  4         130  
4 4     4   15 no warnings 'void';
  4         8  
  4         108  
5              
6 4     4   15 use Carp;
  4         5  
  4         285  
7 4     4   22 use Scalar::Util 'blessed', 'reftype';
  4         4  
  4         342  
8              
9 4     4   1820 use parent 'Exporter::Tiny';
  4         1059  
  4         25  
10             our @EXPORT = our @EXPORT_OK = qw/
11             encode_zpl
12             decode_zpl
13             /;
14              
15              
16             # note: not anchored as-is:
17             our $ValidName = qr/[A-Za-z0-9\$\-_\@.&+\/]+/;
18              
19              
20             sub decode_zpl {
21 15     15 1 6133 my ($str) = @_;
22              
23 15 50       33 confess "Expected a ZPL text string but received no arguments"
24             unless defined $str;
25              
26 15         22 my $root = +{};
27 15         18 my $ref = $root;
28 15         13 my @descended;
29              
30 15         19 my ($level, $lineno) = (0,0);
31              
32 15         292 LINE: for my $line (split /(?:\015?\012)|\015/, $str) {
33 87         66 ++$lineno;
34             # Prep string in-place & skip blank/comments-only:
35 87 100       100 next LINE unless _decode_prepare_line($line);
36              
37             # Manage structure:
38 81         128 _decode_handle_level($lineno, $line, $root, $ref, $level, \@descended);
39              
40             # KV pair:
41 78 100       154 if ( (my $sep_pos = index($line, '=')) > 0 ) {
42 52         73 my ($key, $val) = _decode_parse_kv($lineno, $line, $level, $sep_pos);
43 50         78 _decode_add_kv($lineno, $ref, $key, $val);
44             next LINE
45 49         65 }
46              
47             # New subsection:
48 26 100       317 if (my ($subsect) = $line =~ /^(?:\s+)?($ValidName)(?:\s+?#.*)?$/) {
49 24         44 _decode_add_subsection($lineno, $ref, $subsect, \@descended);
50             next LINE
51 23         42 }
52              
53             confess
54 2         264 "Invalid ZPL (line $lineno); "
55             ."unrecognized syntax or bad section name: '$line'"
56             } # LINE
57              
58             $root
59 6         22 }
60              
61             sub _decode_prepare_line {
62 155     155   345 $_[0] =~ s/\s+$//;
63 155 100 100     728 length($_[0]) == 0 || $_[0] =~ /^(?:\s+)?#/ ? () : 1
64             }
65              
66             sub _decode_handle_level {
67             # ($lineno, $line, $root, $ref, $level, $tree_ref)
68             #
69             # Manage indentation-based hierarchy
70             # Validates indent level
71             # Munges current $ref, $level, $tree_ref in-place
72              
73 137     137   112 my $cur_indent = 0;
74 137         615 $cur_indent++ while substr($_[1], $cur_indent, 1) eq ' ';
75 137 100       199 if ($cur_indent % 4) {
76 1         108 confess
77             "Invalid ZPL (line $_[0]); "
78             ."expected 4-space indent, indent is $cur_indent"
79             }
80              
81 136 100       263 if ($cur_indent == 0) {
    100          
    100          
82 58         53 $_[3] = $_[2];
83 58         44 $_[4] = $cur_indent;
84 58         89 @{ $_[5] } = ();
  58         111  
85             } elsif ($cur_indent > $_[4]) {
86 36 100       94 unless (defined $_[5]->[ ($cur_indent / 4) - 1 ]) {
87 3         340 confess "Invalid ZPL (line $_[0]); no matching parent section"
88             }
89 33         52 $_[4] = $cur_indent;
90             } elsif ($cur_indent < $_[4]) {
91 7         23 my $wanted_idx = ( ($_[4] - $cur_indent) / 4 ) - 1 ;
92 7         11 my $wanted_ref = $_[5]->[$wanted_idx];
93 7 50       15 unless (defined $wanted_ref) {
94 0         0 confess
95             "BUG; cannot find matching parent section"
96             ." [idx = $wanted_idx] [indent = $cur_indent]"
97             }
98 7         6 $_[3] = $wanted_ref;
99 7         7 $_[4] = $cur_indent;
100 7         13 @{ $_[5] } = @{ $_[5] }[ ($wanted_idx + 1) .. $#{ $_[5] } ];
  7         16  
  7         9  
  7         12  
101             }
102             }
103              
104             sub _decode_add_subsection {
105             # ($lineno, $ref, $subsect, \@descended)
106 41 100   41   85 if (exists $_[1]->{ $_[2] }) {
107 1         113 confess "Invalid ZPL (line $_[0]); existing property with this name"
108             }
109 40         31 unshift @{ $_[3] }, $_[1];
  40         60  
110 40         107 $_[1] = $_[1]->{ $_[2] } = +{};
111             }
112              
113              
114             sub _decode_parse_kv {
115             # ($lineno, $line, $level, $sep_pos)
116             #
117             # Takes a line that appears to contain a k = v pair
118             # Returns ($key, $val)
119              
120 90     90   562 my $key = substr $_[1], $_[2], ( $_[3] - $_[2] );
121 90         232 $key =~ s/\s+$//;
122 90 100       491 unless ($key =~ /^$ValidName$/) {
123 1         188 confess "Invalid ZPL (line $_[0]); "
124             ."'$key' is not a valid ZPL property name"
125             }
126              
127 89         118 my $tmpval = substr $_[1], $_[3] + 1;
128 89         150 $tmpval =~ s/^\s+//;
129 89         57 my $realval;
130 89         78 my $maybe_q = substr $tmpval, 0, 1;
131 89 100 100     352 if ( ($maybe_q eq q{'} || $maybe_q eq q{"})
      100        
132             && (my $matching_q_pos = index $tmpval, $maybe_q, 1) > 1 ) {
133             # Quoted, consume up to matching and clean up tmpval
134 16         25 $realval = substr $tmpval, 1, ($matching_q_pos - 1), '';
135 16         17 substr $tmpval, 0, 2, '';
136             } else {
137             # Unquoted or mismatched quotes
138 73         71 my $maybe_trailing = index $tmpval, ' ';
139 73 100       139 $realval = substr $tmpval, 0,
140             ($maybe_trailing > -1 ? $maybe_trailing : length $tmpval),
141             '';
142             }
143              
144 89         76 $tmpval =~ s/#.*$//;
145 89         74 $tmpval =~ s/\s+//;
146 89 100       120 if (length $tmpval) {
147 1         170 confess "Invalid ZPL (line $_[0]); garbage at end-of-line: '$tmpval'"
148             }
149              
150 88         189 ($key, $realval)
151             }
152              
153             sub _decode_add_kv {
154             # ($lineno, $ref, $key, $val)
155             #
156             # Add a value to property; create lists as-needed
157              
158 88 100   88   164 if (exists $_[1]->{ $_[2] }) {
159 15 100       58 if (ref $_[1]->{ $_[2] } eq 'HASH') {
    100          
160 1         102 confess
161             "Invalid ZPL (line $_[0]); existing subsection with this name"
162             } elsif (ref $_[1]->{ $_[2] } eq 'ARRAY') {
163 5         6 push @{ $_[1]->{ $_[2] } }, $_[3]
  5         17  
164             } else {
165 9         27 $_[1]->{ $_[2] } = [ $_[1]->{ $_[2] }, $_[3] ]
166             }
167             return
168 14         21 }
169 73         144 $_[1]->{ $_[2] } = $_[3]
170             }
171              
172              
173             sub encode_zpl {
174 9     9 1 5627 my ($obj) = @_;
175 9 100 66     63 $obj = $obj->TO_ZPL if blessed $obj and $obj->can('TO_ZPL');
176 9 100       200 confess "Expected a HASH but got $obj" unless ref $obj eq 'HASH';
177 8         15 _encode($obj)
178             }
179              
180             sub _encode {
181 17     17   19 my ($ref, $indent) = @_;
182 17   100     42 $indent ||= 0;
183 17         18 my $str = '';
184              
185 17         29 KEY: for my $key (keys %$ref) {
186 33 100       496 confess "$key is not a valid ZPL property name"
187             unless $key =~ qr/^$ValidName$/;
188 31         51 my $val = $ref->{$key};
189            
190 31 100 66     78 if (blessed $val && $val->can('TO_ZPL')) {
191 2         5 $val = $val->TO_ZPL;
192             }
193              
194 31 100       61 if (ref $val eq 'ARRAY') {
195 5         14 $str .= _encode_array($key, $val, $indent);
196             next KEY
197 4         20 }
198              
199 26 100       39 if (ref $val eq 'HASH') {
200 9         12 $str .= ' ' x $indent;
201 9         10 $str .= "$key\n";
202 9         26 $str .= _encode($val, $indent + 4);
203             next KEY
204 9         15 }
205            
206 17 100       450 if (ref $val) {
207 1         97 confess "Do not know how to handle '$val'"
208             }
209              
210 16         22 $str .= ' ' x $indent;
211 16         23 $str .= "$key = " . _maybe_quote($val) . "\n";
212             }
213              
214             $str
215 13         32 }
216              
217             sub _encode_array {
218 5     5   9 my ($key, $ref, $indent) = @_;
219 5         5 my $str = '';
220 5         9 for my $item (@$ref) {
221 11 100       245 confess "ZPL does not support structures of this type in lists: ".ref $item
222             if ref $item;
223 10         13 $str .= ' ' x $indent;
224 10         19 $str .= "$key = " . _maybe_quote($item) . "\n";
225             }
226             $str
227 4         6 }
228              
229             sub _maybe_quote {
230 26     26   25 my ($val) = @_;
231 26 50 66     500 return qq{'$val'}
232             if index($val, q{"}) > -1
233             and index($val, q{'}) == -1;
234 26 100 66     192 return qq{"$val"}
      66        
      66        
      66        
235             if index($val, '#') > -1
236             or index($val, '=') > -1
237             or (index($val, q{'}) > -1 and index($val, q{"}) == -1)
238             or $val =~ /\s/; # last because slow :\
239 22         47 $val
240             }
241              
242             1;
243              
244             =pod
245              
246             =head1 NAME
247              
248             Text::ZPL - Encode and decode ZeroMQ Property Language
249              
250             =head1 SYNOPSIS
251              
252             # Decode ZPL to a HASH:
253             my $data = decode_zpl( $zpl_text );
254             # Encode a HASH to ZPL text:
255             my $zpl = encode_zpl( $data );
256              
257             =head1 DESCRIPTION
258              
259             An implementation of the C, a simple ASCII
260             configuration file format; see L for details.
261              
262             Exports two functions by default: L and L. This
263             module uses L to export functions, which allows for flexible
264             import options; see the L documentation for details.
265              
266             As a simple example, a C file as such:
267              
268             # This is my conf.
269             # There are many like it, but this one is mine.
270             confname = "My Config"
271              
272             context
273             iothreads = 1
274              
275             main
276             publisher
277             bind = tcp://eth0:5550
278             bind = tcp://eth0:5551
279             subscriber
280             connect = tcp://192.168.0.10:5555
281              
282             ... results in a structure like:
283              
284             {
285             confname => "My Config",
286             context => { iothreads => '1' },
287             main => {
288             subscriber => {
289             connect => 'tcp://192.168.0.10:5555'
290             },
291             publisher => {
292             bind => [ 'tcp://eth0:5550', 'tcp://eth0:5551' ]
293             }
294             }
295             }
296              
297             =head2 decode_zpl
298              
299             Given a string of C-encoded text, returns an appropriate Perl C; an
300             exception is thrown if invalid input is encountered.
301              
302             (See L for a streaming interface.)
303              
304             =head2 encode_zpl
305              
306             Given a Perl C, returns an appropriate C-encoded text string; an
307             exception is thrown if the data given cannot be represented in C (see
308             L).
309              
310             =head3 TO_ZPL
311              
312             A blessed object can provide a B method that will supply a plain
313             C or C (but see L) to the encoder:
314              
315             # Shallow-clone this object's backing hash, for example:
316             sub TO_ZPL {
317             my $self = shift;
318             +{ %$self }
319             }
320              
321             =head2 CAVEATS
322              
323             Not all Perl data structures can be represented in ZPL; specifically,
324             deeply-nested structures in an C will throw an exception:
325              
326             # Simple list is OK:
327             encode_zpl(+{ list => [ 1 .. 3 ] });
328             # -> list: 1
329             # list: 2
330             # list: 3
331             # Deeply nested is not representable:
332             encode_zpl(+{
333             list => [
334             'abc',
335             list2 => [1 .. 3]
336             ],
337             });
338             # -> dies
339              
340             Encoding skips empty lists (C references).
341              
342             (The spec is unclear on all this; issues welcome via RT or GitHub!)
343              
344             =head1 SEE ALSO
345              
346             L
347              
348             =head1 AUTHOR
349              
350             Jon Portnoy
351              
352             =cut