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.002001';
3 4     4   46753 use strict; use warnings FATAL => 'all';
  4     4   7  
  4         130  
  4         17  
  4         5  
  4         123  
4 4     4   17 no warnings 'void';
  4         7  
  4         102  
5              
6 4     4   17 use Carp;
  4         4  
  4         305  
7 4     4   18 use Scalar::Util 'blessed', 'reftype';
  4         6  
  4         505  
8              
9 4     4   2077 use parent 'Exporter::Tiny';
  4         1071  
  4         32  
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 5348 my ($str) = @_;
22              
23 15 50       41 confess "Expected a ZPL text string but received no arguments"
24             unless defined $str;
25              
26 15         24 my $root = +{};
27 15         16 my $ref = $root;
28 15         17 my @descended;
29              
30 15         21 my ($level, $lineno) = (0,0);
31              
32 15         277 LINE: for my $line (split /(?:\015?\012)|\015/, $str) {
33 87         72 ++$lineno;
34             # Prep string in-place & skip blank/comments-only:
35 87 100       118 next LINE unless _decode_prepare_line($line);
36              
37             # Manage structure:
38 81         148 _decode_handle_level($lineno, $line, $root, $ref, $level, \@descended);
39              
40             # KV pair:
41 78 100       164 if ( (my $sep_pos = index($line, '=')) > 0 ) {
42 52         79 my ($key, $val) = _decode_parse_kv($lineno, $line, $level, $sep_pos);
43 50         83 _decode_add_kv($lineno, $ref, $key, $val);
44             next LINE
45 49         72 }
46              
47             # New subsection:
48 26 100       308 if (my ($subsect) = $line =~ /^(?:\s+)?($ValidName)(?:\s+?#.*)?$/) {
49 24         42 _decode_add_subsection($lineno, $ref, $subsect, \@descended);
50             next LINE
51 23         46 }
52              
53             confess
54 2         342 "Invalid ZPL (line $lineno); "
55             ."unrecognized syntax or bad section name: '$line'"
56             } # LINE
57              
58             $root
59 6         26 }
60              
61             sub _decode_prepare_line {
62 155     155   482 $_[0] =~ s/\s+$//;
63 155 100 100     950 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   135 my $cur_indent = 0;
74 137         762 $cur_indent++ while substr($_[1], $cur_indent, 1) eq ' ';
75 137 100       254 if ($cur_indent % 4) {
76 1         154 confess
77             "Invalid ZPL (line $_[0]); "
78             ."expected 4-space indent, indent is $cur_indent"
79             }
80              
81 136 100       337 if ($cur_indent == 0) {
    100          
    100          
82 58         72 $_[3] = $_[2];
83 58         66 $_[4] = $cur_indent;
84 58         75 @{ $_[5] } = ();
  58         139  
85             } elsif ($cur_indent > $_[4]) {
86 36 100       127 unless (defined $_[5]->[ ($cur_indent / 4) - 1 ]) {
87 3         516 confess "Invalid ZPL (line $_[0]); no matching parent section"
88             }
89 33         63 $_[4] = $cur_indent;
90             } elsif ($cur_indent < $_[4]) {
91 8         30 my $wanted_idx = ( ($_[4] - $cur_indent) / 4 ) - 1 ;
92 8         15 my $wanted_ref = $_[5]->[$wanted_idx];
93 8 50       18 unless (defined $wanted_ref) {
94 0         0 confess
95             "BUG; cannot find matching parent section"
96             ." [idx = $wanted_idx] [indent = $cur_indent]"
97             }
98 8         10 $_[3] = $wanted_ref;
99 8         10 $_[4] = $cur_indent;
100 8         15 @{ $_[5] } = @{ $_[5] }[ ($wanted_idx + 1) .. $#{ $_[5] } ];
  8         25  
  8         16  
  8         18  
101             }
102             }
103              
104             sub _decode_add_subsection {
105             # ($lineno, $ref, $subsect, \@descended)
106 41 100   41   113 if (exists $_[1]->{ $_[2] }) {
107 1         154 confess "Invalid ZPL (line $_[0]); existing property with this name"
108             }
109 40         36 unshift @{ $_[3] }, $_[1];
  40         77  
110 40         145 $_[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   657 my $key = substr $_[1], $_[2], ( $_[3] - $_[2] );
121 90         312 $key =~ s/\s+$//;
122 90 100       719 unless ($key =~ /^$ValidName$/) {
123 1         175 confess "Invalid ZPL (line $_[0]); "
124             ."'$key' is not a valid ZPL property name"
125             }
126              
127 89         159 my $tmpval = substr $_[1], $_[3] + 1;
128 89         195 $tmpval =~ s/^\s+//;
129 89         86 my $realval;
130 89         95 my $maybe_q = substr $tmpval, 0, 1;
131 89 100 100     442 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         35 $realval = substr $tmpval, 1, ($matching_q_pos - 1), '';
135 16         20 substr $tmpval, 0, 2, '';
136             } else {
137             # Unquoted or mismatched quotes
138 73         101 my $maybe_trailing = index $tmpval, ' ';
139 73 100       180 $realval = substr $tmpval, 0,
140             ($maybe_trailing > -1 ? $maybe_trailing : length $tmpval),
141             '';
142             }
143              
144 89         103 $tmpval =~ s/#.*$//;
145 89         95 $tmpval =~ s/\s+//;
146 89 100       159 if (length $tmpval) {
147 1         184 confess "Invalid ZPL (line $_[0]); garbage at end-of-line: '$tmpval'"
148             }
149              
150 88         453 ($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   208 if (exists $_[1]->{ $_[2] }) {
159 15 100       71 if (ref $_[1]->{ $_[2] } eq 'HASH') {
    100          
160 1         104 confess
161             "Invalid ZPL (line $_[0]); existing subsection with this name"
162             } elsif (ref $_[1]->{ $_[2] } eq 'ARRAY') {
163 5         9 push @{ $_[1]->{ $_[2] } }, $_[3]
  5         19  
164             } else {
165 9         40 $_[1]->{ $_[2] } = [ $_[1]->{ $_[2] }, $_[3] ]
166             }
167             return
168 14         28 }
169 73         194 $_[1]->{ $_[2] } = $_[3]
170             }
171              
172              
173             sub encode_zpl {
174 9     9 1 5156 my ($obj) = @_;
175 9 100 66     70 $obj = $obj->TO_ZPL if blessed $obj and $obj->can('TO_ZPL');
176 9 100       242 confess "Expected a HASH but got $obj" unless ref $obj eq 'HASH';
177 8         14 _encode($obj)
178             }
179              
180             sub _encode {
181 17     17   25 my ($ref, $indent) = @_;
182 17   100     49 $indent ||= 0;
183 17         20 my $str = '';
184              
185 17         40 KEY: for my $key (keys %$ref) {
186 33 100       645 confess "$key is not a valid ZPL property name"
187             unless $key =~ qr/^$ValidName$/;
188 31         67 my $val = $ref->{$key};
189            
190 31 100 66     98 if (blessed $val && $val->can('TO_ZPL')) {
191 2         6 $val = $val->TO_ZPL;
192             }
193              
194 31 100       68 if (ref $val eq 'ARRAY') {
195 5         14 $str .= _encode_array($key, $val, $indent);
196             next KEY
197 4         8 }
198              
199 26 100       45 if (ref $val eq 'HASH') {
200 9         16 $str .= ' ' x $indent;
201 9         12 $str .= "$key\n";
202 9         41 $str .= _encode($val, $indent + 4);
203             next KEY
204 9         16 }
205            
206 17 100       512 if (ref $val) {
207 1         185 confess "Do not know how to handle '$val'"
208             }
209              
210 16         27 $str .= ' ' x $indent;
211 16         29 $str .= "$key = " . _maybe_quote($val) . "\n";
212             }
213              
214             $str
215 13         37 }
216              
217             sub _encode_array {
218 5     5   9 my ($key, $ref, $indent) = @_;
219 5         8 my $str = '';
220 5         9 for my $item (@$ref) {
221 11 100       252 confess "ZPL does not support structures of this type in lists: ".ref $item
222             if ref $item;
223 10         16 $str .= ' ' x $indent;
224 10         537 $str .= "$key = " . _maybe_quote($item) . "\n";
225             }
226             $str
227 4         10 }
228              
229             sub _maybe_quote {
230 26     26   26 my ($val) = @_;
231 26 50 66     66 return qq{'$val'}
232             if index($val, q{"}) > -1
233             and index($val, q{'}) == -1;
234 26 100 66     244 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         57 $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             =head2 encode_zpl
303              
304             Given a Perl C, returns an appropriate C-encoded text string; an
305             exception is thrown if the data given cannot be represented in C (see
306             L).
307              
308             =head3 TO_ZPL
309              
310             A blessed object can provide a B method that will supply a plain
311             C or C (but see L) to the encoder:
312              
313             # Shallow-clone this object's backing hash, for example:
314             sub TO_ZPL {
315             my $self = shift;
316             +{ %$self }
317             }
318              
319             =head2 CAVEATS
320              
321             Not all Perl data structures can be represented in ZPL; specifically,
322             deeply-nested structures in an C will throw an exception:
323              
324             # Simple list is OK:
325             encode_zpl(+{ list => [ 1 .. 3 ] });
326             # -> list: 1
327             # list: 2
328             # list: 3
329             # Deeply nested is not representable:
330             encode_zpl(+{
331             list => [
332             'abc',
333             list2 => [1 .. 3]
334             ],
335             });
336             # -> dies
337              
338             Encoding skips empty lists (C references).
339              
340             (The spec is unclear on all this; issues welcome via RT or GitHub!)
341              
342             =head1 AUTHOR
343              
344             Jon Portnoy
345              
346             =cut