File Coverage

blib/lib/BERT/Encoder.pm
Criterion Covered Total %
statement 129 135 95.5
branch 50 60 83.3
condition 7 9 77.7
subroutine 25 25 100.0
pod 2 17 11.7
total 213 246 86.5


line stmt bran cond sub pod time code
1             package BERT::Encoder;
2 6     6   46 use strict;
  6         11  
  6         255  
3 6     6   32 use warnings;
  6         19  
  6         169  
4              
5 6     6   164 use 5.008;
  6         17  
  6         249  
6              
7 6     6   33 use Carp 'croak';
  6         11  
  6         388  
8 6     6   33 use BERT::Constants;
  6         10  
  6         512  
9 6     6   31 use BERT::Types;
  6         11  
  6         895  
10              
11             # stolen from Regexp::Common :-)
12             use constant {
13 6         9767 INT_RE => qr/^(?:(?:[+-]?)(?:[0123456789]+))$/,
14             FLOAT_RE => qr/^(?:(?i)(?:[+-]?)(?:(?=[.]?[0123456789])(?:[0123456789]*)(?:(?:[.])(?:[0123456789]{0,}))?)(?:(?:[E])(?:(?:[+-]?)(?:[0123456789]+))|))$/,
15 6     6   35 };
  6         10  
16              
17             sub new {
18 35     35 1 51 my $class = shift;
19 35         129 return bless { }, $class;
20             }
21              
22             sub encode {
23 35     35 1 60 my ($self, $value) = @_;
24 35         94 return pack('C', MAGIC_NUMBER) . $self->encode_any($value);
25             }
26              
27             sub encode_any {
28 97     97 0 128 my ($self, $value) = @_;
29              
30 97 100       197 return $self->encode_nil unless defined $value;
31              
32 95         131 my $type = ref $value;
33 95 100       469 if ($type eq 'ARRAY') { return $self->encode_array($value) }
  13 100       40  
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
34 2         7 elsif ($type eq 'HASH') { return $self->encode_dict($value) }
35 1         5 elsif ($type eq 'Regexp') { return $self->encode_regex($value) }
36 29         68 elsif ($type eq 'BERT::Atom') { return $self->encode_atom($value) }
37 14         76 elsif ($type eq 'BERT::Tuple') { return $self->encode_tuple($value) }
38 2         6 elsif ($type eq 'BERT::Boolean') { return $self->encode_boolean($value) }
39 2         8 elsif ($type eq 'BERT::Dict') { return $self->encode_dict($value) }
40 1         5 elsif ($type eq 'BERT::Time') { return $self->encode_time($value) }
41 4         15 elsif ($type eq 'Math::BigInt') { return $self->encode_integer($value) }
42 0         0 elsif ($type) { croak "Can't encode type $type" }
43              
44             # I didn't use B::svref_2object on this because by only looking at variables
45             # in Perl can actually modify them
46 27 100       167 if ($value =~ INT_RE) { return $self->encode_integer($value) }
  15 100       41  
47 2         44 elsif ($value =~ FLOAT_RE) { return $self->encode_float($value) }
48 10         33 else { return $self->encode_binary($value) }
49             }
50              
51             sub encode_nil {
52 2     2 0 3 my ($self) = @_;
53 2         7 my $perl = BERT::Tuple->new([BERT::Atom->new('bert'), BERT::Atom->new('nil')]);
54 2         6 return $self->encode_any($perl);
55             }
56              
57             sub is_erl_string {
58 10     10 0 15 my ($self, $value) = @_;
59            
60             # Although it works I'm not sure it's the best way to test whether a
61             # scalar is within the byte range
62 10         13 foreach my $item (@{ $value }) {
  10         21  
63 17 100       66 if ($item =~ /^\d+$/) {
64 9 50 33     49 return 0 if 0 > $item or $item > 255;
65             } else {
66 8 100       38 return 0 if length $item != 1;
67             }
68             }
69 3         28 return 1;
70             }
71              
72             sub encode_array {
73 13     13 0 20 my ($self, $value) = @_;
74 13         18 my @value = @{ $value };
  13         31  
75            
76 13 100       49 return pack('C', NIL_EXT) unless @value;
77 10 100       30 return $self->encode_bytelist(\@value) if $self->is_erl_string(\@value);
78              
79 7         29 my $array = $self->encode_list(\@value, []);
80 7         16 return pack('CN', LIST_EXT, scalar @{ $array }) . join('', @{ $array }) . pack('C', NIL_EXT);
  7         18  
  7         58  
81             }
82              
83             sub encode_list {
84 73     73 0 111 my ($self, $value, $array) = @_;
85              
86 73 100       69 if (@{ $value }) {
  73         148  
87 52         40 my $head = shift @{ $value };
  52         78  
88 52         62 return $self->encode_list($value, [@{ $array }, $self->encode_any($head)]);
  52         169  
89             } else {
90 21         197 return $array;
91             }
92             }
93              
94             sub encode_dict {
95 4     4 0 6 my ($self, $value) = @_;
96              
97 4         4 my @array;
98 4 100       15 my @value = ref $value eq 'BERT::Dict' ? @{ $value->value } : %{ $value };
  2         7  
  2         8  
99 4         19 while (my @key_value = splice(@value, 0, 2)) {
100 3         20 push @array, BERT::Tuple->new(\@key_value);
101             }
102              
103 4         28 my $perl = BERT::Tuple->new([BERT::Atom->new('bert'), BERT::Atom->new('dict'), \@array]);
104 4         11 return $self->encode_any($perl);
105             }
106              
107             sub encode_regex {
108 1     1 0 4 my ($self, $value) = @_;
109              
110 1         3 for ($value) { s/^\(\?//; s/\)$// }
  1         8  
  1         4  
111 1         6 my ($modifiers, $pattern) = split /:/, $value, 2;
112 1         3 my ($on, $off) = split /-/, $modifiers;
113              
114 1         2 my @options;
115 1         3 for ($on) {
116 1 50       4 if (/i/) { push @options, BERT::Atom->new('caseless') }
  1 0       5  
    0          
    0          
117 0         0 elsif (/s/) { push @options, BERT::Atom->new('dotall') }
118 0         0 elsif (/x/) { push @options, BERT::Atom->new('extended') }
119 0         0 elsif (/m/) { push @options, BERT::Atom->new('multiline') }
120             }
121              
122 1         5 my $perl = BERT::Tuple->new([BERT::Atom->new('bert'), BERT::Atom->new('regex'), $pattern, \@options]);
123 1         4 return $self->encode_any($perl);
124             }
125              
126             sub encode_atom {
127 29     29 0 81 my ($self, $value) = @_;
128 29         105 return pack('Cna*', ATOM_EXT, length $value, $value);
129             }
130              
131             sub encode_tuple {
132 14     14 0 24 my ($self, $value) = @_;
133              
134 14         14 my @array = @{ $value->value };
  14         46  
135 14 50       61 return pack('C*', SMALL_TUPLE_EXT, scalar @array) . join('', @{ $self->encode_list(\@array, []) }) if @array < 256;
  14         40  
136 0         0 return pack('CN', LARGE_TUPLE_EXT, scalar @array) . join('', @{ $self->encode_list(\@array, []) });
  0         0  
137             }
138              
139             sub encode_bytelist {
140 3     3 0 6 my ($self, $value) = @_;
141 3         7 return pack('CnC*', STRING_EXT, scalar @{ $value }, @{ $value });
  3         5  
  3         23  
142             }
143              
144             sub encode_boolean {
145 2     2 0 2 my ($self, $value) = @_;
146              
147 2 100       6 my $boolean = $value ? BERT::Atom->new('true') : BERT::Atom->new('false');
148 2         8 my $perl = BERT::Tuple->new([BERT::Atom->new('bert'), $boolean]);
149 2         6 return $self->encode_any($perl);
150             }
151              
152             sub encode_time {
153 1     1 0 3 my ($self, $value) = @_;
154              
155 6     6   5815 use integer;
  6         67  
  6         32  
156 1         4 my ($seconds, $microseconds) = $value->value;
157 1         3 my $megaseconds = $seconds / 1_000_000;
158 1         15 $seconds = $seconds % 1_000_000;
159 1         18 my $perl = BERT::Tuple->new([BERT::Atom->new('bert'), BERT::Atom->new('time'), $megaseconds, $seconds, $microseconds]);
160 1         4 return $self->encode_any($perl);
161             }
162              
163             sub encode_integer {
164 19     19 0 26 my ($self, $value) = @_;
165              
166 19 100 100     148 return pack('C2', SMALL_INTEGER_EXT, $value)
167             if 0 <= $value and $value <= 255;
168              
169             # I think newer versions of erlang no longer have the 28bit limit,
170             # so maybe I should add an option to extend the limit to max_int
171 11 100 100     844 return pack('CN', INTEGER_EXT, $value)
172             if ERL_MIN <= $value and $value <= ERL_MAX;
173              
174 4 100       522 my $sign = $value < 0 ? 1 : 0;
175 4         445 $value = abs($value);
176              
177 4         112 my @bytes;
178 4         13 while ($value > 0) {
179 274         101526 push @bytes, $value & 0xFF;
180 274         93958 $value >>= 8;
181             }
182              
183 4 100       1329 return pack('C*', SMALL_BIG_EXT, scalar @bytes, $sign, @bytes) if @bytes < 256;
184 1         14 return pack('CNC*', LARGE_BIG_EXT, scalar @bytes, $sign, @bytes);
185             }
186              
187             sub encode_float {
188 2     2 0 5 my ($self, $value) = @_;
189 2         33 return pack('CZ31', FLOAT_EXT, sprintf('%.20e', $value));
190             }
191              
192             sub encode_binary {
193 10     10 0 14 my ($self, $value) = @_;
194 10         88 return pack('CNa*', BINARY_EXT, length $value, $value);
195             }
196              
197             1;
198              
199             __END__