File Coverage

lib/Badger/Codec/TT.pm
Criterion Covered Total %
statement 60 74 81.0
branch 25 36 69.4
condition 5 9 55.5
subroutine 6 6 100.0
pod 3 3 100.0
total 99 128 77.3


line stmt bran cond sub pod time code
1             package Badger::Codec::TT;
2              
3             use Badger::Class
4 1         7 version => 0.01,
5             debug => 0,
6             base => 'Badger::Codec Badger::Prototype',
7             import => 'class CLASS',
8             utils => 'blessed textlike',
9             constants => 'ARRAY HASH',
10             messages => {
11             badref => 'Cannot encode %s reference',
12 1     1   487 };
  1         3  
13              
14             our $MATCH_ESCAPED = qr/ \\([\\']) /x;
15             our $MATCH_WORD = qr/ (\w+) /x;
16             our $MATCH_QUOTE = qr/ '( (?:\\[\\'] | . | \n)*? )' /sx;
17             our $MATCH_QUOTED = qr/ \G \s* $MATCH_QUOTE /sx;
18             our $MATCH_KEY = qr/ \G \s* (?: $MATCH_WORD | $MATCH_QUOTE )/sx;
19             our $MATCH_NUMBER = qr/ \G \s* ( -? \d+ (?: \.\d+ )? ) /x;
20             our $MATCH_COMMA = qr/ \G \s* (,\s*)? /x;
21             our $MATCH_ASSIGN = qr/ \G \s* (:|=>?) \s* /x;
22             our $MATCH_HASH = qr/ \G \s* \{ /x;
23             our $MATCH_END_HASH = qr/ \G \s* \} /x;
24             our $MATCH_LIST = qr/ \G \s* \[ /x;
25             our $MATCH_END_LIST = qr/ \G \s* (\] | $) /x; # special case
26             our $MATCH_UNDEF = qr/ \G \s* undef /x;
27              
28             our $ASSIGN = '=';
29             our $COMMA = ' ';
30              
31             sub init {
32 6     6 1 12 my ($self, $config) = @_;
33 6   66     60 $self->{ assign } = $config->{ assign } || $ASSIGN;
34 6   66     22 $self->{ comma } = $config->{ comma } || $COMMA;
35 6         19 return $self;
36             }
37              
38             sub encode {
39 6     6 1 48 shift->prototype->_encode(@_);
40             }
41              
42             sub decode {
43 4     4 1 35 shift->prototype->_decode(@_);
44             }
45              
46             sub _encode {
47 27     27   29 my $self = shift;
48 27         29 my $data = shift;
49            
50 27         22 $self->debug("encoding: $data\n") if DEBUG;
51              
52             # object may have stringification method
53 27 50 33     58 if (blessed $data && textlike $data) {
54 0         0 $data = '' . $data; # drop-through to string handler below
55             }
56              
57 27 50       63 if (! defined $data) {
    100          
    100          
    50          
58 0         0 $self->debug("encoding undef") if DEBUG;
59 0         0 return 'undef';
60             }
61             elsif (! ref $data) {
62 17 100       110 if ($data =~ /^ $MATCH_NUMBER $/ox) {
63 7         11 $self->debug("encoding number: $data") if DEBUG;
64 7         12 return $data;
65             }
66             else {
67             # escape any single quotes or backslashes in the value before quoting it
68 10         10 $self->debug("encoding text: $data") if DEBUG;
69 10         16 $data =~ s/(['\\])/\\$1/g;
70 10         31 return "'" . $data . "'";
71             }
72             }
73             elsif (ref $data eq ARRAY) {
74 3         4 $self->debug("encoding list") if DEBUG;
75 3         6 return '[' . join($self->{ comma }, map { _encode($self, $_) } @$data) . ']';
  7         16  
76             }
77             elsif (ref $data eq HASH) {
78 7         8 $self->debug("encoding hash") if DEBUG;
79 7         14 my $a = $self->{ assign };
80 7         9 my ($k, $v);
81             return
82             '{'
83             . join($self->{ comma },
84             map {
85 7         33 $k = $_;
  14         19  
86 14         27 $v = _encode($self, $data->{$k});
87 14 50       33 if ($k =~ /\W/) {
88 0         0 $k =~ s/(['\\])/\\$1/g;
89 0         0 $k = "'" . $k . "'";
90             }
91 14         70 $k . $a . $v; # key = value
92             }
93             sort keys %$data
94             )
95             . '}';
96             }
97             else {
98 0         0 return $self->error_msg( bad_ref => ref $data );
99             }
100             }
101              
102             sub _decode {
103 48     48   52 my $self = shift;
104 48 100       72 my $text = ref $_[0] ? $_[0] : \$_[0];
105 48         49 my ($key, $value);
106              
107 48         42 if (DEBUG) {
108             my $pos = pos $$text;
109             if ($pos) {
110             $$text =~ /\G(.*)/;
111             $self->debug("decoding: $1\n");
112             pos $$text = $pos;
113             }
114             else {
115             $self->debug("decoding: $$text\n");
116             }
117             }
118              
119 48 100       227 if ($$text =~ /$MATCH_HASH/cog) {
    100          
    100          
    50          
    0          
120 11         13 $self->debug("matched hash\n") if DEBUG;
121 11         13 $value = { };
122 11         33 while ($$text =~ /$MATCH_KEY/cog) {
123 23 50       43 if (defined $1) {
124 23         32 $key = $1;
125             }
126             else {
127 0         0 $key = $2;
128 0         0 $key =~ s/$MATCH_ESCAPED/$1/og;
129             }
130 23 50       61 $$text =~ /$MATCH_ASSIGN/cog
131             || return $self->error("Missing value after $key");
132 23         46 $value->{ $key } = _decode($self, $text);
133 23         78 $$text =~ /$MATCH_COMMA/cog;
134             }
135 11 50       32 $$text =~ /$MATCH_END_HASH/cog
136             || return $self->error("missing } at end of hash definition");
137             }
138             elsif ($$text =~ /$MATCH_LIST/cog) {
139 10         11 $self->debug("matched list\n") if DEBUG;
140 10         24 $value = [ ];
141 10         12 while (1) {
142 31 100       71 if ($$text =~ /$MATCH_END_LIST/cog) {
143 10 50       25 last if $1 eq ']';
144 0         0 return $self->error("missing ] at end of list definition ($1)");
145             }
146 21         38 push(@$value, _decode($self, $text));
147 21         51 $$text =~ /$MATCH_COMMA/cog;
148             }
149             }
150             elsif ($$text =~ /$MATCH_QUOTED/cog) {
151 21         23 $self->debug("matched quoted\n") if DEBUG;
152 21         32 $value = $1;
153 21         31 $value =~ s/$MATCH_ESCAPED/$1/og;
154 21         18 $self->debug("found quoted string: $value\n") if DEBUG;
155             }
156             elsif ($$text =~ /$MATCH_NUMBER/cog) {
157 6         19 $self->debug("matched number") if DEBUG;
158 6         10 $value = $1;
159 6         8 $self->debug("found number: $value\n") if DEBUG;
160             }
161             elsif ($$text =~ /$MATCH_UNDEF/cog) {
162 0         0 $self->debug("matched undef") if DEBUG;
163 0         0 $value = undef;
164             }
165             else {
166 0         0 $self->debug("matched other") if DEBUG;
167 0         0 $$text =~ /\G(.*)/;
168 0         0 return $self->error("bad value: $1")
169             }
170 48         104 return $value;
171             }
172              
173             1;
174              
175             =head1 NAME
176              
177             Badger::Codec::TT - encode/decode data using TT data syntax
178              
179             =head1 SYNOPSIS
180              
181             use Badger::Codec::TT;
182             my $codec = Badger::Codec::TT->new();
183             my $encoded = $codec->encode({ msg => "Hello World" });
184             my $decoded = $codec->decode($encoded);
185              
186             =head1 DESCRIPTION
187              
188             This module implements a subclass of L which encodes and
189             decodes data to and from an extended form of the data definition syntax used
190             in the Template Toolkit. It mainly exists for testing purposes (so that we
191             don't require people to install YAML or JSON just to run some of the Badger
192             tests) and to support some legacy systems that use data encoded in this way
193             (mostly dating back to the days before YAML and JSON were around). If you're
194             starting out afresh then you're probably better off using YAML or JSON unless
195             you have good reason not to.
196              
197             The syntax is similar to Perl in that it uses single quotes for literal
198             strings, square brackets for list definitions and curly braces for hash
199             definitions along with the C<=E> "fat comma" operator to separate hash
200             keys and values. Data structures can be nested indefinitely. The unquoted
201             C token can be used to explicitly represent the undefined value.
202              
203             {
204             message => 'Hello World, this is some text',
205             things => ['a list', 'of some things'],
206             stuff => {
207             pi => 3.14,
208             foo => [ { nested => 'hash' }, ['nested', 'list' ] ],
209             nul => undef,
210             },
211             }
212              
213             TT syntax is more liberal than Perl. It allows you to use C<=> instead
214             of C<=E> to separate keys and values in hash arrays, and commas between
215             items are optional.
216              
217             {
218             message = 'Hello World, this is some text'
219             things = ['a list' 'of some things']
220             stuff = {
221             pi = 3.14
222             foo = [ { nested = 'hash' } ['nested' 'list' ] ]
223             nul = undef
224             }
225             }
226              
227             It will also accept C<:> as a delimiter between hash keys and values,
228             thus providing an overlap with a useful subset of JSON syntax:
229              
230             {
231             message: 'Hello World, this is some text',
232             things: ['a list' 'of some things'],
233             stuff: {
234             pi: 3.14,
235             foo: [ { nested: 'hash' }, ['nested', 'list' ] ],
236             nul: undef
237             }
238             }
239              
240             The decoder is very liberal in what it will accept for delimiters. You can mix
241             and match any of the above styles in the same document if you really want to.
242             However, you would be utterly batshit insane to do such a thing, let alone
243             want for it. Just because we'll accept any of the commonly used formats
244             doesn't mean that you should be using them all at once.
245              
246             {
247             perl => 'Perl looks like this',
248             tt = 'TT looks like this'
249             json: 'JSON looks like this
250             }
251              
252             Note that while the syntax may be more liberal than either Perl or JSON,
253             the semantics are decidedly stricter. It is not possible to embed arbitrary
254             Perl code, instantiate Javascript objects, or do anything else outside of
255             defining vanilla data structures.
256              
257             The encoder generates TT syntax by default (C<=> for assignment, with a single
258             space to delimiter items). You can change these options using the C
259             and C configuration options.
260              
261             my $codec = Badger::Codec::TT->new( assign => '=>', comma => ',' );
262             print $codec->encode($some_data);
263              
264             =head1 METHODS
265              
266             =head2 encode($data)
267              
268             Encodes the Perl data in C<$data> to a TT string.
269              
270             $encoded = Badger::Codec::TT->encode($data);
271              
272             =head2 decode($tt)
273              
274             Decodes the encoded TT string in C<$tt> back into a Perl data structure.
275              
276             $decoded = Badger::Codec::TT->decode($encoded);
277              
278             =head2 encoder()
279              
280             This method returns a reference to an encoding subroutine.
281              
282             my $sub = Badger::Codec::TT->encoder;
283             $encoded = $sub->($data);
284              
285             =head2 decoder()
286              
287              
288             This method returns a reference to a decoding subroutine.
289              
290             my $sub = Badger::Codec::TT->decoder;
291             $decoded = $sub->($encoded);
292              
293             =head1 INTERNAL SUBROUTINES
294              
295             =head2 _encode($data)
296              
297             This internal subroutine performs the recursive encoding of the data.
298              
299             =head2 _decode($tt)
300              
301             This internal subroutine performs the recursive decoding of the data.
302              
303             =head1 AUTHOR
304              
305             Andy Wardley L
306              
307             =head1 COPYRIGHT
308              
309             Copyright (C) 2005-2009 Andy Wardley. All rights reserved.
310              
311             =head1 SEE ALSO
312              
313             L, L, L