File Coverage

blib/lib/Protocol/Redis.pm
Criterion Covered Total %
statement 118 120 98.3
branch 23 28 82.1
condition n/a
subroutine 22 22 100.0
pod 6 6 100.0
total 169 176 96.0


line stmt bran cond sub pod time code
1             package Protocol::Redis;
2              
3 1     1   6 use strict;
  1         2  
  1         26  
4 1     1   5 use warnings;
  1         2  
  1         25  
5 1     1   26 use 5.008_001;
  1         3  
6              
7             our $VERSION = 1.0006;
8              
9             require Carp;
10              
11             sub new {
12 1     1 1 39 my $class = shift;
13 1 50       3 $class = ref $class if ref $class;
14              
15 1         2 my $self = {@_};
16              
17 1 50       5 return unless $self->{api} == '1';
18              
19 1         2 bless $self, $class;
20              
21 1         5 $self->on_message(delete $self->{on_message});
22 1         2 $self->{_messages} = [];
23              
24 1         2 $self->{_state} = \&_state_new_message;
25              
26 1         3 $self;
27             }
28              
29             sub api {
30 1     1 1 2 my $self = shift;
31              
32 1         5 $self->{api};
33             }
34              
35             my %message_type_encoders = (
36             '+' => \&_encode_string,
37             '-' => \&_encode_string,
38             ':' => \&_encode_string,
39             '$' => \&_encode_bulk,
40             '*' => \&_encode_multi_bulk,
41             );
42              
43             sub encode {
44 16     16 1 30 my ($self, $message) = @_;
45              
46 16 50       42 if (my $encoder = $message_type_encoders{$message->{type}}) {
47 16         38 $encoder->($self, $message);
48             }
49             else {
50 0         0 Carp::croak(qq/Unknown message type $message->{type}/);
51             }
52             }
53              
54             sub _encode_string {
55 3     3   6 my ($self, $message) = @_;
56              
57 3         17 $message->{type} . $message->{data} . "\r\n";
58             }
59              
60             sub _encode_bulk {
61 8     8   29 my ($self, $message) = @_;
62              
63 8         13 my $data = $message->{data};
64              
65 8 100       27 return '$-1' . "\r\n"
66             unless defined $data;
67              
68 6         22 '$' . length($data) . "\r\n" . $data . "\r\n";
69             }
70              
71             sub _encode_multi_bulk {
72 5     5   10 my ($self, $message) = @_;
73              
74 5         8 my $data = $message->{data};
75              
76 5 100       15 return '*-1' . "\r\n"
77             unless defined $data;
78              
79 4         13 my $e_message = '*' . scalar(@$data) . "\r\n";
80 4         8 foreach my $element (@$data) {
81 6         14 $e_message .= $self->encode($element);
82             }
83              
84 4         18 $e_message;
85             }
86              
87              
88             sub get_message {
89 26     26 1 39 shift @{$_[0]->{_messages}};
  26         170  
90             }
91              
92             sub on_message {
93 4     4 1 10 my ($self, $cb) = @_;
94 4         9 $self->{_on_message_cb} = $cb;
95             }
96              
97             sub parse {
98 30     30 1 61 my ($self, $chunk) = @_;
99              
100             # Pass chunk to current vertex.
101             # Some vertices can return unparsed chunk. In this case
102             # cycle will pass chunk to next vertex.
103 30         66 1 while $chunk = $self->{_state}->($self, $chunk);
104             }
105              
106             sub _message_parsed {
107 29     29   47 my ($self, $chunk) = @_;
108              
109 29         44 my $message = delete $self->{_cmd};
110              
111 29 100       58 if (my $cb = $self->{_on_message_cb}) {
112 4         10 $cb->($self, $message);
113             }
114             else {
115 25         35 push @{$self->{_messages}}, $message;
  25         45  
116             }
117              
118 29         50 $self->{_state} = \&_state_new_message;
119 29         111 $chunk;
120             }
121              
122             my %message_type_parsers = (
123             '+' => \&_state_string_message,
124             '-' => \&_state_string_message,
125             ':' => \&_state_string_message,
126             '$' => \&_state_bulk_message,
127             '*' => \&_state_multibulk_message,
128             );
129              
130             sub _state_parse_message_type {
131 39     39   66 my ($self, $chunk) = @_;
132              
133 39         88 my $cmd = substr $chunk, 0, 1, '';
134              
135 39 50       96 if ($cmd) {
136 39 50       88 if (my $parser = $message_type_parsers{$cmd}) {
137 39         64 $self->{_cmd}{type} = $cmd;
138 39         57 $self->{_state} = $parser;
139 39         130 return $chunk;
140             }
141              
142 0         0 Carp::croak(qq/Unexpected input "$cmd"/);
143             }
144             }
145              
146             sub _state_new_message {
147 29     29   48 my ($self, $chunk) = @_;
148              
149 29         68 $self->{_cmd} = {type => undef, data => undef};
150              
151 29         55 $self->{_state_cb} = \&_message_parsed;
152              
153 29         51 $self->{_state} = \&_state_parse_message_type;
154 29         96 $chunk;
155             }
156              
157             sub _state_string_message {
158 44     44   71 my ($self, $chunk) = @_;
159              
160 44         90 my $str = $self->{_state_string} .= $chunk;
161 44         74 my $i = index $str, "\r\n";
162              
163             # string isn't full
164 44 100       99 return if $i < 0;
165              
166             # We got full string
167 39         77 $self->{_cmd}{data} = substr $str, 0, $i, '';
168              
169             # Delete newline
170 39         54 substr $str, 0, 2, '';
171              
172 39         69 delete $self->{_state_string};
173              
174 39         76 $self->{_state_cb}->($self, $str);
175             }
176              
177             sub _state_bulk_message {
178 19     19   37 my ($self, $chunk) = @_;
179              
180 19         21 my $bulk_state_cb = $self->{_state_cb};
181              
182             # Read bulk message size
183             $self->{_state_cb} = sub {
184 19     19   35 my ($self, $chunk) = @_;
185              
186 19         30 $self->{_bulk_size} = delete $self->{_cmd}{data};
187              
188 19 100       51 if ($self->{_bulk_size} == -1) {
189              
190             # Nil
191 1         3 $self->{_cmd}{data} = undef;
192 1         3 $bulk_state_cb->($self, $chunk);
193             }
194             else {
195 18         28 $self->{_state_cb} = $bulk_state_cb;
196 18         29 $self->{_state} = \&_state_bulk_message_data;
197 18         89 $chunk;
198             }
199 19         62 };
200 19         37 $self->{_state} = \&_state_string_message;
201 19         53 $chunk;
202             }
203              
204             sub _state_bulk_message_data {
205 19     19   33 my ($self, $chunk) = @_;
206              
207 19         37 my $str = $self->{_state_string} .= $chunk;
208              
209             # String + newline parsed
210 19 100       50 return unless length $str >= $self->{_bulk_size} + 2;
211              
212 18         37 $self->{_cmd}{data} = substr $str, 0, $self->{_bulk_size}, '';
213              
214             # Delete ending newline
215 18         27 substr $str, 0, 2, '';
216              
217 18         28 delete $self->{_state_string};
218 18         36 delete $self->{_bulk_size};
219              
220 18         37 $self->{_state_cb}->($self, $str);
221             }
222              
223             sub _state_multibulk_message {
224 8     8   14 my ($self, $chunk) = @_;
225              
226 8         16 my $mbulk_state_cb = delete $self->{_state_cb};
227 8         13 my $data = [];
228 8         13 my $mbulk_process;
229              
230             my $arguments_num;
231              
232             $mbulk_process = sub {
233 10     10   17 my ($self, $chunk) = @_;
234              
235             push @$data,
236             { type => delete $self->{_cmd}{type},
237             data => delete $self->{_cmd}{data}
238 10         28 };
239              
240 10 100       24 if (scalar @$data == $arguments_num) {
241              
242             # Cleanup
243 6         8 $mbulk_process = undef;
244 6         10 delete $self->{_state_cb};
245              
246             # Return message
247 6         8 $self->{_cmd}{type} = '*';
248 6         12 $self->{_cmd}{data} = $data;
249 6         11 $mbulk_state_cb->($self, $chunk);
250             }
251             else {
252              
253             # read next string
254 4         6 $self->{_state_cb} = $mbulk_process;
255 4         7 $self->{_state} = \&_state_parse_message_type;
256 4         15 $chunk;
257             }
258 8         28 };
259              
260             $self->{_state_cb} = sub {
261 8     8   33 my ($self, $chunk) = @_;
262              
263             # Number of Multi-Bulk message
264 8         13 $arguments_num = delete $self->{_cmd}{data};
265 8 100       34 if ($arguments_num < 1) {
266 2         4 $mbulk_process = undef;
267 2 100       10 $self->{_cmd}{data} = $arguments_num == 0 ? [] : undef;
268 2         9 $mbulk_state_cb->($self, $chunk);
269             }
270             else {
271              
272             # We got messages
273 6         11 $self->{_state_cb} = $mbulk_process;
274 6         12 $self->{_state} = \&_state_parse_message_type;
275 6         34 $chunk;
276             }
277 8         20 };
278              
279             # Get number of messages
280 8         14 $self->{_state} = \&_state_string_message;
281 8         27 $chunk;
282             }
283              
284             1;
285             __END__