File Coverage

blib/lib/Text/ZPL.pm
Criterion Covered Total %
statement 122 122 100.0
branch 54 56 96.4
condition 30 32 93.7
subroutine 16 16 100.0
pod 2 2 100.0
total 224 228 98.2


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