File Coverage

blib/lib/Protocol/Redis.pm
Criterion Covered Total %
statement 188 200 94.0
branch 108 118 91.5
condition 44 50 88.0
subroutine 14 14 100.0
pod 6 6 100.0
total 360 388 92.7


line stmt bran cond sub pod time code
1             package Protocol::Redis;
2              
3 1     1   9 use strict;
  1         2  
  1         49  
4 1     1   11 use warnings;
  1         2  
  1         75  
5 1     1   22 use 5.008_001;
  1         4  
6              
7             our $VERSION = '2.0002';
8              
9             require Carp;
10              
11             sub new {
12 6     6 1 132 my $class = shift;
13 6 50       57 $class = ref $class if ref $class;
14              
15 6         18 my $self = {@_};
16              
17             Carp::croak(qq/Unknown Protocol::Redis API version $self->{api}/)
18             unless $self->{api} == 1
19             or $self->{api} == 2
20 6 100 66     766 or $self->{api} == 3;
      100        
21              
22 3         7 bless $self, $class;
23              
24 3         19 $self->on_message(delete $self->{on_message});
25 3         7 $self->{_messages} = [];
26              
27 3         10 $self;
28             }
29              
30             sub api {
31 3     3 1 7 my $self = shift;
32              
33 3         16 $self->{api};
34             }
35              
36             sub encode {
37 55     55 1 330 my ($self) = @_;
38 55 100       236 return $self->{api} == 3 ? _encode_resp3(@_) : _encode_resp2(@_);
39             }
40              
41             sub get_message {
42 171     171 1 239 shift @{$_[0]->{_messages}};
  171         1579  
43             }
44              
45             sub on_message {
46 12     12 1 27 my ($self, $cb) = @_;
47 12   100     106 $self->{_on_message_cb} = $cb || \&_gather_messages;
48             }
49              
50             sub parse {
51 246     246 1 550 my ($self) = @_;
52 246 100       857 return $self->{api} == 3 ? _parse_resp3(@_) : _parse_resp2(@_);
53             }
54              
55             sub _gather_messages {
56 168     168   227 push @{$_[0]->{_messages}}, $_[1];
  168         420  
57             }
58              
59             my %simple_types = ('+' => 1, '-' => 1, ':' => 1);
60              
61             sub _encode_resp2 {
62 22     22   26 my $self = shift;
63              
64 22         25 my $encoded_message = '';
65 22         48 while (@_) {
66 34         37 my $message = shift;
67              
68             # Bulk string
69 34 100       75 if ($message->{type} eq '$') {
    100          
    50          
70 18 100       26 if (defined $message->{data}) {
71             $encoded_message
72             .= '$'
73             . length($message->{data}) . "\r\n"
74 14         40 . $message->{data} . "\r\n";
75             }
76             else {
77 4         13 $encoded_message .= "\$-1\r\n";
78             }
79             }
80              
81             # Array (multi bulk)
82             elsif ($message->{type} eq '*') {
83 10 100       15 if (defined $message->{data}) {
84             $encoded_message
85 8         9 .= '*' . scalar(@{$message->{data}}) . "\r\n";
  8         15  
86 8         31 unshift @_, @{$message->{data}};
  8         20  
87             }
88             else {
89 2         10 $encoded_message .= "*-1\r\n";
90             }
91             }
92              
93             # String, error, integer
94             elsif (exists $simple_types{$message->{type}}) {
95 6         29 $encoded_message .= $message->{type} . $message->{data} . "\r\n";
96             }
97             else {
98 0         0 Carp::croak(qq/Unknown message type $message->{type}/);
99             }
100             }
101              
102 22         98 return $encoded_message;
103             }
104              
105             sub _parse_resp2 {
106 80     80   113 my $self = shift;
107 80         147 $self->{_buffer} .= shift;
108              
109 80   100     171 my $message = $self->{_message} ||= {};
110 80         109 my $buffer = \$self->{_buffer};
111              
112             CHUNK:
113 80         227 while ((my $pos = index($$buffer, "\r\n")) != -1) {
114              
115             # Check our state: are we parsing new message or completing existing
116 122 100       215 if (!$message->{type}) {
117 114 50       216 if ($pos < 1) {
118 0         0 Carp::croak(qq/Unexpected input "$$buffer"/);
119             }
120              
121 114         228 $message->{type} = substr $$buffer, 0, 1;
122 114         184 $message->{_argument} = substr $$buffer, 1, $pos - 1;
123 114         177 substr $$buffer, 0, $pos + 2, ''; # Remove type + argument + \r\n
124             }
125              
126             # Simple Strings, Errors, Integers
127 122 100       267 if (exists $simple_types{$message->{type}}) {
    100          
    50          
128 34         73 $message->{data} = delete $message->{_argument};
129             }
130              
131             # Bulk Strings
132             elsif ($message->{type} eq '$') {
133 64 100       182 if ($message->{_argument} eq '-1') {
    100          
134 2         5 $message->{data} = undef;
135             }
136             elsif (length($$buffer) >= $message->{_argument} + 2) {
137             $message->{data} = substr $$buffer, 0, $message->{_argument},
138 54         116 '';
139 54         76 substr $$buffer, 0, 2, ''; # Remove \r\n
140             }
141             else {
142             return # Wait more data
143 8         21 }
144             }
145              
146             # Arrays
147             elsif ($message->{type} eq '*') {
148 24 100       43 if ($message->{_argument} eq '-1') {
149 2         6 $message->{data} = undef;
150             }
151             else {
152 22         34 $message->{data} = [];
153 22 100       47 if ($message->{_argument} > 0) {
154 20         36 $message = $self->{_message} = {_parent => $message};
155 20         50 next;
156             }
157             }
158             }
159              
160             # Invalid input
161             else {
162 0         0 Carp::croak(qq/Unexpected input "$self->{_message}{type}"/);
163             }
164              
165 94         132 delete $message->{_argument};
166 94         109 delete $self->{_message};
167              
168             # Fill parents with data
169 94         186 while (my $parent = delete $message->{_parent}) {
170 44         45 push @{$parent->{data}}, $message;
  44         65  
171              
172 44 100       46 if (@{$parent->{data}} < $parent->{_argument}) {
  44         76  
173 24         35 $message = $self->{_message} = {_parent => $parent};
174 24         63 next CHUNK;
175             }
176             else {
177 20         24 $message = $parent;
178 20         47 delete $parent->{_argument};
179             }
180             }
181              
182 70         135 $self->{_on_message_cb}->($self, $message);
183 70         245 $message = $self->{_message} = {};
184             }
185             }
186              
187             my %blob_types = ('$' => 1, '!' => 1, '=' => 1);
188             my %aggregate_types = ('*' => 1, '%' => 1, '~' => 1, '|' => 1, '>' => 1);
189              
190             sub _encode_resp3 {
191 33     33   63 my $self = shift;
192              
193 33         63 my $encoded_message = '';
194 33         99 while (@_) {
195 58         86 my $message = shift;
196              
197             # Attributes
198 58 100 100     472 if (defined $message->{attributes}) {
    100 100        
    100 100        
    100          
    100          
    100          
    100          
    50          
199 4         17 my %append_message = %$message;
200 4         12 my $attributes = delete $append_message{attributes};
201 4         14 $encoded_message .= '|' . keys(%$attributes) . "\r\n";
202             unshift @_, (
203 4         16 map { ({type => '$', data => $_}, $attributes->{$_}) }
  4         35  
204             sort keys %$attributes
205             ),
206             \%append_message;
207             }
208              
209             # Bulk string, Blob error, Verbatim string
210             elsif (exists $blob_types{$message->{type}}) {
211 15         32 my $text = $message->{data};
212 15 100       49 if ($message->{type} eq '=') {
213             my $format =
214 3 100       14 defined $message->{format} ? $message->{format} : 'txt';
215 3         9 $text = "$format:$text";
216             }
217             $encoded_message
218 15         62 .= $message->{type} . length($text) . "\r\n" . $text . "\r\n";
219             }
220              
221             # Array, Set, Push
222             elsif ($message->{type} eq '*'
223             or $message->{type} eq '~'
224             or $message->{type} eq '>')
225             {
226             $encoded_message
227 8         17 .= $message->{type} . scalar(@{$message->{data}}) . "\r\n";
  8         30  
228 8         16 unshift @_, @{$message->{data}};
  8         27  
229             }
230              
231             # Map
232             elsif ($message->{type} eq '%') {
233 1 50       7 if (ref $message->{data} eq 'ARRAY') {
234             $encoded_message
235 0         0 .= $message->{type} . int(@{$message->{data}} / 2) . "\r\n";
  0         0  
236 0         0 unshift @_, @{$message->{data}};
  0         0  
237             }
238             else {
239             $encoded_message
240 1         4 .= $message->{type} . keys(%{$message->{data}}) . "\r\n";
  1         5  
241             unshift @_,
242 1         9 map { ({type => '$', data => $_}, $message->{data}{$_}) }
243 1         4 sort keys %{$message->{data}};
  1         5  
244             }
245             }
246              
247             # String, error, integer, big number
248             elsif (exists $simple_types{$message->{type}}
249             or $message->{type} eq '(')
250             {
251 16         58 $encoded_message .= $message->{type} . $message->{data} . "\r\n";
252             }
253              
254             # Double
255             elsif ($message->{type} eq ',') {
256              
257             # inf
258 9 100 100     101 if ( $message->{data} != 0
    100          
259             and $message->{data} == $message->{data} * 2)
260             {
261             $encoded_message
262 2 100       14 .= ',' . ($message->{data} > 0 ? '' : '-') . "inf\r\n";
263             }
264              
265             # nan
266             elsif ($message->{data} != $message->{data}) {
267 1         5 $encoded_message .= ",nan\r\n";
268             }
269             else {
270             $encoded_message
271 6         26 .= $message->{type} . $message->{data} . "\r\n";
272             }
273             }
274              
275             # Null
276             elsif ($message->{type} eq '_') {
277 2         11 $encoded_message .= "_\r\n";
278             }
279              
280             # Boolean
281             elsif ($message->{type} eq '#') {
282 3 100       17 $encoded_message .= '#' . ($message->{data} ? 't' : 'f') . "\r\n";
283             }
284             else {
285 0         0 Carp::croak(qq/Unknown message type $message->{type}/);
286             }
287             }
288              
289 33         255 return $encoded_message;
290             }
291              
292             sub _parse_resp3 {
293 166     166   325 my $self = shift;
294 166         462 $self->{_buffer} .= shift;
295              
296 166   100     482 my $message = $self->{_message} ||= {};
297 166         365 my $buffer = \$self->{_buffer};
298              
299             CHUNK:
300 166         593 while ((my $pos = index($$buffer, "\r\n")) != -1) {
301              
302             # Check our state: are we parsing new message or completing existing
303 279 100       720 if (!$message->{type}) {
304 266 50       644 if ($pos < 1) {
305 0         0 Carp::croak(qq/Unexpected input "$$buffer"/);
306             }
307              
308 266         856 $message->{type} = substr $$buffer, 0, 1;
309 266         653 $message->{_argument} = substr $$buffer, 1, $pos - 1;
310 266         673 substr $$buffer, 0, $pos + 2, ''; # Remove type + argument + \r\n
311             }
312              
313             # Streamed String Parts - must be checked for first
314 279 100 33     1800 if ($message->{_streaming}) {
    100 33        
    100          
    100          
    100          
    100          
    100          
    100          
    50          
315 34 50       79 unless ($message->{type} eq ';') {
316 0         0 Carp::croak(qq/Unexpected input "$message->{type}"/);
317             }
318              
319 34 100       102 if ($message->{_argument} == 0) {
    50          
320 4         10 $message = delete $message->{_streaming};
321             }
322             elsif (length($$buffer) >= $message->{_argument} + 2) {
323 30         56 my $streaming = delete $message->{_streaming};
324             $streaming->{data} .= substr $$buffer, 0,
325 30         106 $message->{_argument}, '';
326 30         64 substr $$buffer, 0, 2, ''; # Remove \r\n
327 30         97 $message = $self->{_message} = {_streaming => $streaming};
328 30         178 next;
329             }
330             else {
331             return # Wait more data
332 0         0 }
333             }
334              
335             # Simple Strings, Errors, Integers
336             elsif (exists $simple_types{$message->{type}}) {
337 62         142 $message->{data} = delete $message->{_argument};
338             }
339              
340             # Null
341             elsif ($message->{type} eq '_') {
342 2         5 delete $message->{_argument};
343 2         20 $message->{data} = undef;
344             }
345              
346             # Booleans
347             elsif ($message->{type} eq '#') {
348 2         5 $message->{data} = !!(delete($message->{_argument}) eq 't');
349             }
350              
351             # Doubles
352             elsif ($message->{type} eq ',') {
353 17         64 $message->{data} = delete $message->{_argument};
354 17 100       108 $message->{data} = 'nan' if $message->{data} =~ m/^[-+]?nan/i;
355             }
356              
357             # Big Numbers
358             elsif ($message->{type} eq '(') {
359 3         2308 require Math::BigInt;
360             $message->{data} =
361 3         35260 Math::BigInt->new(delete $message->{_argument});
362             }
363              
364             # Bulk/Blob Strings, Blob Errors, Verbatim Strings
365             elsif (exists $blob_types{$message->{type}}) {
366 106 100 100     653 if ($message->{type} eq '$' and $message->{_argument} eq '?') {
    100          
367 4         12 $message->{data} = '';
368 4         18 $message = $self->{_message} = {_streaming => $message};
369 4         20 next;
370             }
371             elsif (length($$buffer) >= $message->{_argument} + 2) {
372             $message->{data} = substr $$buffer, 0, $message->{_argument},
373 89         282 '';
374 89 100 66     370 if ( $message->{type} eq '='
375             and $message->{data} =~ s/^(.{3})://s)
376             {
377 9         40 $message->{format} = $1;
378             }
379 89         186 substr $$buffer, 0, 2, ''; # Remove \r\n
380             }
381             else {
382             return # Wait more data
383 13         62 }
384             }
385              
386             # Arrays, Maps, Sets, Attributes, Push
387             elsif (exists $aggregate_types{$message->{type}}) {
388 49 100 100     293 if ($message->{type} eq '%' or $message->{type} eq '|') {
389 9         24 $message->{data} = {};
390             }
391             else {
392 40         121 $message->{data} = [];
393             }
394              
395 49 100 100     254 if ($message->{_argument} eq '?' or $message->{_argument} > 0) {
396 44         140 $message = $self->{_message} = {_parent => $message};
397 44         175 next;
398             }
399              
400             # Populate empty attributes for next message if we reach here
401 5 100       24 if ($message->{type} eq '|') {
402 1         5 $message->{attributes} = {};
403 1         3 delete $message->{type};
404 1         2 delete $message->{data};
405 1         3 delete $message->{_argument};
406 1         5 next;
407             }
408             }
409              
410             # Streamed Aggregate End
411             elsif ( $message->{type} eq '.'
412             and $message->{_parent}
413             and $message->{_parent}{_argument} eq '?')
414             {
415 4         9 $message = delete $message->{_parent};
416 4         9 delete $message->{_elements};
417             }
418              
419             # Invalid input
420             else {
421 0         0 Carp::croak(qq/Unexpected input "$self->{_message}{type}"/);
422             }
423              
424 187         100059 delete $message->{_argument};
425 187         390 delete $self->{_message};
426              
427             # Fill parents with data
428 187         572 while (my $parent = delete $message->{_parent}) {
429              
430             # Map key or value
431 112 100 100     416 if ($parent->{type} eq '%' or $parent->{type} eq '|') {
432 26 100       65 if (exists $parent->{_key}) {
433 13         27 $parent->{_elements}++;
434 13         40 $parent->{data}{delete $parent->{_key}} = $message;
435             }
436             else {
437 13         36 $parent->{_key} = $message->{data};
438             }
439             }
440              
441             # Array or set element
442             else {
443 86         156 $parent->{_elements}++;
444 86         138 push @{$parent->{data}}, $message;
  86         251  
445             }
446              
447             # Do we need more elements?
448 112 100 100     559 if ($parent->{_argument} eq '?'
      100        
449             or ($parent->{_elements} || 0) < $parent->{_argument})
450             {
451 72         272 $message = $self->{_message} = {_parent => $parent};
452 72         340 next CHUNK;
453             }
454              
455             # Aggregate is complete
456 40         79 $message = $parent;
457 40         114 delete $message->{_argument};
458 40         70 delete $message->{_elements};
459 40         73 delete $message->{_key};
460              
461             # Attributes apply to the following message
462 40 100       186 if ($message->{type} eq '|') {
463 5         14 $self->{_message} = $message;
464 5         12 $message->{attributes} = delete $message->{data};
465 5         11 delete $message->{type};
466 5         21 next CHUNK;
467             }
468             }
469              
470 110         316 $self->{_on_message_cb}->($self, $message);
471 110         560 $message = $self->{_message} = {};
472             }
473             }
474              
475             1;
476             __END__