File Coverage

blib/lib/TAPx/Parser/YAML.pm
Criterion Covered Total %
statement 159 187 85.0
branch 83 118 70.3
condition 10 12 83.3
subroutine 14 18 77.7
pod 6 6 100.0
total 272 341 79.7


line stmt bran cond sub pod time code
1             package TAPx::Parser::YAML;
2              
3 3     3   29696 use 5.005;
  3         13  
  3         118  
4 3     3   15 use strict;
  3         4  
  3         112  
5              
6 3     3   15 use vars qw{$VERSION @ISA @EXPORT_OK $errstr};
  3         8  
  3         310  
7              
8             BEGIN {
9 3     3   10 $VERSION = '0.50_07';
10 3         6 $errstr = '';
11              
12 3         14 require Exporter;
13 3         48 @ISA = qw{ Exporter };
14 3         14546 @EXPORT_OK = qw{ Load Dump };
15             }
16              
17             # Create the main error hash
18             my %ERROR = (
19             YAML_PARSE_ERR_NO_FINAL_NEWLINE =>
20             "Stream does not end with newline character",
21              
22             );
23              
24             my %NO = (
25             '%' => 'TAPx::Parser::YAML does not support directives',
26             '&' => 'TAPx::Parser::YAML does not support anchors',
27             '*' => 'TAPx::Parser::YAML does not support aliases',
28             '?' => 'TAPx::Parser::YAML does not support explicit mapping keys',
29             ':' => 'TAPx::Parser::YAML does not support explicit mapping values',
30             '!' => 'TAPx::Parser::YAML does not support explicit tags',
31             );
32              
33             my $ESCAPE_CHAR = '[\\x00-\\x08\\x0b-\\x0d\\x0e-\\x1f\"\n]';
34              
35             # Escapes for unprintable characters
36             my @UNPRINTABLE = qw(z x01 x02 x03 x04 x05 x06 a
37             x08 t n v f r x0e x0f
38             x10 x11 x12 x13 x14 x15 x16 x17
39             x18 x19 x1a e x1c x1d x1e x1f
40             );
41              
42             # Printable characters for escapes
43             my %UNESCAPES = (
44             z => "\x00", a => "\x07", t => "\x09",
45             n => "\x0a", v => "\x0b", f => "\x0c",
46             r => "\x0d", e => "\x1b", '\\' => '\\',
47             );
48              
49             # Create an empty TAPx::Parser::YAML object
50             sub new {
51 0     0 1 0 my $class = shift;
52 0         0 bless [@_], $class;
53             }
54              
55             # Create an object from a file
56             sub read {
57 4 50   4 1 710 my $class = ref $_[0] ? ref shift: shift;
58              
59             # Check the file
60 4 50       17 my $file = shift
61             or return $class->_error('You did not specify a file name');
62 4 50       74 return $class->_error("File '$file' does not exist") unless -e $file;
63 4 50       15 return $class->_error("'$file' is a directory, not a file") unless -f _;
64 4 50       21 return $class->_error("Insufficient permissions to read '$file'")
65             unless -r _;
66              
67             # Slurp in the file
68 4         20 local $/ = undef;
69 4 50       169 open CFG, $file
70             or return $class->_error("Failed to open file '$file': $!");
71 4         123 my $contents = ;
72 4         100 close CFG;
73              
74 4         24 $class->read_string($contents);
75             }
76              
77             # Create an object from a string
78             sub read_string {
79 58 50   58 1 13657 my $class = ref $_[0] ? ref shift: shift;
80 58         219 my $self = bless [], $class;
81              
82             # Handle special cases
83 58 50       180 return undef unless defined $_[0];
84 58 100       133 return $self unless length $_[0];
85 54 50       293 unless ( $_[0] =~ /[\012\015]+$/ ) {
86 0         0 return $class->_error('YAML_PARSE_ERR_NO_FINAL_NEWLINE');
87             }
88              
89             # Split the file into lines
90 54         877 my @lines = grep { !/^\s*(?:\#.+)?$/ }
  175         609  
91             split /(?:\015{1,2}\012|\015|\012)/, shift;
92              
93             # A nibbling parser
94 54         153 while (@lines) {
95              
96             # Do we have a document header?
97 58 100       242 if ( $lines[0] =~ /^---(?:\s*(.+)\s*)?$/ ) {
98              
99             # Handle scalar documents
100 56         76 shift @lines;
101 56 100       151 if ( defined $1 ) {
102 17         171 push @$self, $self->_read_scalar( "$1", [undef], \@lines );
103 17         53 next;
104             }
105             }
106              
107 41 100 100     265 if ( !@lines or $lines[0] =~ /^---(?:\s*(.+)\s*)?$/ ) {
    100          
    50          
108              
109             # A naked document
110 11         35 push @$self, undef;
111              
112             }
113             elsif ( $lines[0] =~ /^\s*\-/ ) {
114              
115             # An array at the root
116 18         24 my $document = [];
117 18         33 push @$self, $document;
118 18         46 $self->_read_array( $document, [0], \@lines );
119              
120             }
121             elsif ( $lines[0] =~ /^(\s*)\w/ ) {
122              
123             # A hash at the root
124 12         22 my $document = {};
125 12         29 push @$self, $document;
126 12         59 $self->_read_hash( $document, [ length($1) ], \@lines );
127              
128             }
129             else {
130 0         0 die "CODE INCOMPLETE (are you sure this is a YAML file?)";
131             }
132             }
133              
134 54         176 $self;
135             }
136              
137             sub _check_support {
138              
139             # Check if we support the next char
140 0     0   0 my $errstr = $NO{ substr( $_[1], 0, 1 ) };
141 0 0       0 Carp::croak($errstr) if $errstr;
142             }
143              
144             # Deparse a scalar string to the actual scalar
145             sub _read_scalar {
146 111     111   168 my ( $self, $string, $indent, $lines ) = @_;
147 111 100       259 return undef if $string eq '~';
148 94 100       199 if ( $string =~ /^'(.*?)'$/ ) {
149 10 50       20 return '' unless defined $1;
150 10         14 my $rv = $1;
151 10         14 $rv =~ s/''/'/g;
152 10         40 return $rv;
153             }
154 84 100       198 if ( $string =~ /^"((?:\\.|[^"])*)"$/ ) {
155 5         10 my $str = $1;
156 5         8 $str =~ s/\\"/"/g;
157 5         14 $str
158 3 50       13 =~ s/\\([never\\fartz]|x([0-9a-fA-F]{2}))/(length($1)>1)?pack("H2",$2):$UNESCAPES{$1}/gex;
159 5         17 return $str;
160             }
161 79 50       169 if ( $string =~ /^['"]/ ) {
162              
163             # A quote with folding... we don't support that
164 0         0 die "TAPx::Parser::YAML does not support multi-line quoted scalars";
165             }
166 79 100 100     318 unless ( $string eq '>' or $string eq '|' ) {
167              
168             # Regular unquoted string
169 77         361 return $string;
170             }
171              
172             # Error
173 2 50       6 die "Multi-line scalar content missing" unless @$lines;
174              
175             # Check the indent depth
176 2         5 $lines->[0] =~ /^(\s*)/;
177 2         5 $indent->[-1] = length("$1");
178 2 50 33     27 if ( defined $indent->[-2] and $indent->[-1] <= $indent->[-2] ) {
179 0         0 die "Illegal line indenting";
180             }
181              
182             # Pull the lines
183 2         4 my @multiline = ();
184 2         5 while (@$lines) {
185 6         15 $lines->[0] =~ /^(\s*)/;
186 6 100       17 last unless length($1) >= $indent->[-1];
187 5         18 push @multiline, substr( shift(@$lines), length($1) );
188             }
189              
190 2 100       18 join( ( $string eq '>' ? ' ' : "\n" ), @multiline ) . "\n";
191             }
192              
193             # Parse an array
194             sub _read_array {
195 22     22   41 my ( $self, $array, $indent, $lines ) = @_;
196              
197 22         43 while (@$lines) {
198 44         87 $lines->[0] =~ /^(\s*)/;
199 44 100       142 if ( length($1) < $indent->[-1] ) {
    50          
200 2         7 return 1;
201             }
202             elsif ( length($1) > $indent->[-1] ) {
203 0         0 die "Hash line over-indented";
204             }
205              
206 42 100       225 if ( $lines->[0] =~ /^(\s*\-\s+)\S+\s*:(?:\s+|$)/ ) {
    100          
    50          
207              
208             # Inline nested hash
209 3         5 my $indent2 = length("$1");
210 3         12 $lines->[0] =~ s/-/ /;
211 3         4 push @$array, {};
212 3         30 $self->_read_hash( $array->[-1], [ @$indent, $indent2 ], $lines );
213              
214             }
215             elsif ( $lines->[0] =~ /^\s*\-(\s*)(.+?)\s*$/ ) {
216              
217             # Array entry with a value
218 32         35 shift @$lines;
219 32         118 push @$array,
220             $self->_read_scalar( "$2", [ @$indent, undef ], $lines );
221              
222             }
223             elsif ( $lines->[0] =~ /^\s*\-\s*$/ ) {
224 7         7 shift @$lines;
225 7 50       28 if ( $lines->[0] =~ /^(\s*)\-/ ) {
    50          
226 0         0 my $indent2 = length("$1");
227 0 0       0 if ( $indent->[-1] == $indent2 ) {
228              
229             # Null array entry
230 0         0 push @$array, undef;
231             }
232             else {
233              
234             # Naked indenter
235 0         0 push @$array, [];
236 0         0 $self->_read_array(
237             $array->[-1], [ @$indent, $indent2 ],
238             $lines
239             );
240             }
241              
242             }
243             elsif ( $lines->[0] =~ /^(\s*)\w/ ) {
244 7         12 push @$array, {};
245 7         23 $self->_read_hash(
246             $array->[-1], [ @$indent, length("$1") ],
247             $lines
248             );
249              
250             }
251             else {
252 0         0 die "CODE INCOMPLETE";
253             }
254              
255             }
256             else {
257 0         0 die "CODE INCOMPLETE";
258             }
259             }
260              
261 20         99 return 1;
262             }
263              
264             # Parse an array
265             sub _read_hash {
266 28     28   47 my ( $self, $hash, $indent, $lines ) = @_;
267              
268 28         61 while (@$lines) {
269 85         172 $lines->[0] =~ /^(\s*)/;
270 85 100       259 if ( length($1) < $indent->[-1] ) {
    50          
271 13         41 return 1;
272             }
273             elsif ( length($1) > $indent->[-1] ) {
274 0         0 die "Hash line over-indented";
275             }
276              
277             # Get the key
278 72 50       337 unless ( $lines->[0] =~ s/^\s*(\S+)\s*:(\s+|$)// ) {
279 0         0 die "Bad hash line";
280             }
281 72         137 my $key = $1;
282              
283             # Do we have a value?
284 72 100       117 if ( length $lines->[0] ) {
285              
286             # Yes
287 62         182 $hash->{$key} = $self->_read_scalar(
288             shift(@$lines), [ @$indent, undef ],
289             $lines
290             );
291             }
292             else {
293              
294             # An indent
295 10         13 shift @$lines;
296 10 100       45 if ( $lines->[0] =~ /^(\s*)-/ ) {
    50          
297 4         13 $hash->{$key} = [];
298 4         20 $self->_read_array(
299             $hash->{$key}, [ @$indent, length($1) ],
300             $lines
301             );
302             }
303             elsif ( $lines->[0] =~ /^(\s*)./ ) {
304 6         12 my $indent2 = length("$1");
305 6 50       11 if ( $indent->[-1] == $indent2 ) {
306              
307             # Null hash entry
308 0         0 $hash->{$key} = undef;
309             }
310             else {
311 6         14 $hash->{$key} = {};
312 6         27 $self->_read_hash(
313             $hash->{$key},
314             [ @$indent, length($1) ], $lines
315             );
316             }
317             }
318             }
319             }
320              
321 15         52 return 1;
322             }
323              
324             # Save an object to a file
325             sub write {
326 1     1 1 2 my $self = shift;
327 1 50       4 my $file = shift or return $self->_error('No file name provided');
328              
329             # Write it to the file
330 1 50       652 open( CFG, '>' . $file )
331             or return $self->_error("Failed to open file '$file' for writing: $!");
332 1         6 print CFG $self->write_string;
333 1         66 close CFG;
334             }
335              
336             # Save an object to a string
337             sub write_string {
338 28     28 1 20931 my $self = shift;
339 28 100       78 return '' unless @$self;
340              
341             # Iterate over the documents
342 25         28 my $indent = 0;
343 25         38 my @lines = ();
344 25         47 foreach my $cursor (@$self) {
345 29         48 push @lines, '---';
346              
347             # An empty document
348 29 100       126 if ( !defined $cursor ) {
    100          
    100          
    50          
349              
350             # Do nothing
351              
352             # A scalar document
353             }
354             elsif ( !ref $cursor ) {
355 7         17 $lines[-1] .= $self->_write_scalar($cursor);
356              
357             # A list at the root
358             }
359             elsif ( ref $cursor eq 'ARRAY' ) {
360 9         26 push @lines, $self->_write_array( $indent, $cursor );
361              
362             # A hash at the root
363             }
364             elsif ( ref $cursor eq 'HASH' ) {
365 6         17 push @lines, $self->_write_hash( $indent, $cursor );
366              
367             }
368             else {
369 0         0 die "CODE INCOMPLETE";
370             }
371             }
372              
373 25         50 join '', map {"$_\n"} @lines;
  86         212  
374             }
375              
376             sub _write_scalar {
377 54     54   62 my $str = $_[1];
378 54 100       104 return '~' unless defined $str;
379 47 100       149 if ( $str =~ /$ESCAPE_CHAR/ ) {
380 2         4 $str =~ s/\\/\\\\/g;
381 2         3 $str =~ s/"/\\"/g;
382 2         6 $str =~ s/\n/\\n/g;
383 2         4 $str =~ s/([\x00-\x1f])/\\$UNPRINTABLE[ord($1)]/ge;
  0         0  
384 2         8 return qq{"$str"};
385             }
386 45 100 100     197 if ( length($str) == 0 or $str =~ /\s/ ) {
387 7         11 $str =~ s/'/''/;
388 7         22 return "'$str'";
389             }
390 38         80 return $str;
391             }
392              
393             sub _write_array {
394 11     11   18 my ( $self, $indent, $array ) = @_;
395 11         17 my @lines = ();
396 11         18 foreach my $el (@$array) {
397 21         36 my $line = ( ' ' x $indent ) . '-';
398 21 100       45 if ( !ref $el ) {
    50          
    50          
399 16         34 $line .= ' ' . $self->_write_scalar($el);
400 16         41 push @lines, $line;
401              
402             }
403             elsif ( ref $el eq 'ARRAY' ) {
404 0         0 push @lines, $line;
405 0         0 push @lines, $self->_write_array( $indent + 1, $el );
406              
407             }
408             elsif ( ref $el eq 'HASH' ) {
409 5         9 push @lines, $line;
410 5         11 push @lines, $self->_write_hash( $indent + 1, $el );
411              
412             }
413             else {
414 0         0 die "CODE INCOMPLETE";
415             }
416             }
417              
418 11         45 @lines;
419             }
420              
421             sub _write_hash {
422 14     14   22 my ( $self, $indent, $hash ) = @_;
423 14         15 my @lines = ();
424 14         54 foreach my $name ( sort keys %$hash ) {
425 36         44 my $el = $hash->{$name};
426 36         61 my $line = ( ' ' x $indent ) . "$name:";
427 36 100       65 if ( !ref $el ) {
    100          
    50          
428 31         50 $line .= ' ' . $self->_write_scalar($el);
429 31         64 push @lines, $line;
430              
431             }
432             elsif ( ref $el eq 'ARRAY' ) {
433 2         4 push @lines, $line;
434 2         5 push @lines, $self->_write_array( $indent + 1, $el );
435              
436             }
437             elsif ( ref $el eq 'HASH' ) {
438 3         4 push @lines, $line;
439 3         11 push @lines, $self->_write_hash( $indent + 1, $el );
440              
441             }
442             else {
443 0         0 die "CODE INCOMPLETE";
444             }
445             }
446              
447 14         58 @lines;
448             }
449              
450             # Set error
451             sub _error {
452 0 0   0     $errstr = $ERROR{ $_[1] } ? "$ERROR{$_[1]} ($_[1])" : $_[1];
453 0           undef;
454             }
455              
456             # Retrieve error
457             sub errstr {
458 0     0 1   $errstr;
459             }
460              
461             1;
462              
463             __END__