File Coverage

blib/lib/Mango/BSON.pm
Criterion Covered Total %
statement 173 175 98.8
branch 101 106 95.2
condition 5 8 62.5
subroutine 43 43 100.0
pod 19 19 100.0
total 341 351 97.1


line stmt bran cond sub pod time code
1             package Mango::BSON;
2 11     11   221033 use Mojo::Base -strict;
  11         39  
  11         75  
3              
4 11     11   1272 use re 'regexp_pattern';
  11         21  
  11         2034  
5 11     11   74 use Carp 'croak';
  11         19  
  11         481  
6 11     11   66 use Exporter 'import';
  11         19  
  11         316  
7 11     11   4847 use Mango::BSON::Binary;
  11         27  
  11         60  
8 11     11   4526 use Mango::BSON::Code;
  11         25  
  11         88  
9 11     11   4475 use Mango::BSON::Document;
  11         32  
  11         318  
10 11     11   4408 use Mango::BSON::Number;
  11         28  
  11         67  
11 11     11   4625 use Mango::BSON::ObjectID;
  11         31  
  11         130  
12 11     11   4829 use Mango::BSON::Time;
  11         39  
  11         98  
13 11     11   4930 use Mango::BSON::Timestamp;
  11         34  
  11         80  
14 11     11   4960 use Mojo::JSON;
  11         175705  
  11         546  
15 11     11   94 use Scalar::Util 'blessed';
  11         23  
  11         2384  
16              
17             my @BSON = (
18             qw(bson_bin bson_code bson_dbref bson_decode bson_doc bson_double),
19             qw(bson_encode bson_false bson_int32 bson_int64 bson_length bson_max),
20             qw(bson_min bson_oid bson_raw bson_time bson_true bson_ts)
21             );
22             our @EXPORT_OK = (@BSON, 'encode_cstring');
23             our %EXPORT_TAGS = (bson => \@BSON);
24              
25             # Types
26             use constant {
27 11         2511 DOUBLE => "\x01",
28             STRING => "\x02",
29             DOCUMENT => "\x03",
30             ARRAY => "\x04",
31             BINARY => "\x05",
32             UNDEFINED => "\x06",
33             OBJECT_ID => "\x07",
34             BOOL => "\x08",
35             DATETIME => "\x09",
36             NULL => "\x0a",
37             REGEX => "\x0b",
38             CODE => "\x0d",
39             CODE_SCOPE => "\x0f",
40             INT32 => "\x10",
41             TIMESTAMP => "\x11",
42             INT64 => "\x12",
43             MIN_KEY => "\xff",
44             MAX_KEY => "\x7f"
45 11     11   79 };
  11         21  
46              
47             # Binary subtypes
48             use constant {
49 11         26131 BINARY_GENERIC => "\x00",
50             BINARY_FUNCTION => "\x01",
51             BINARY_UUID => "\x04",
52             BINARY_MD5 => "\x05",
53             BINARY_USER_DEFINED => "\x80"
54 11     11   70 };
  11         22  
55              
56             # The pack() format to use for each numeric type
57             my %num_pack_fmt = (
58             DOUBLE() => 'd<',
59             INT32() => 'l<',
60             INT64() => 'q<'
61             );
62              
63             # Reuse boolean singletons
64             my $FALSE = Mojo::JSON->false;
65             my $TRUE = Mojo::JSON->true;
66             my $BOOL = blessed $TRUE;
67              
68             my $MAXKEY = bless {}, 'Mango::BSON::_MaxKey';
69             my $MINKEY = bless {}, 'Mango::BSON::_MinKey';
70              
71 14     14 1 3401 sub bson_bin { Mango::BSON::Binary->new(data => shift) }
72              
73 4     4 1 600 sub bson_code { Mango::BSON::Code->new(code => shift) }
74              
75 1     1 1 11 sub bson_dbref { bson_doc('$ref' => shift, '$id' => shift) }
76              
77             sub bson_decode {
78 62     62 1 947 my $bson = shift;
79 62 100       118 return undef unless my $len = bson_length($bson);
80 59 100       154 return length $bson == $len ? _decode_doc(\$bson) : undef;
81             }
82              
83             sub bson_doc {
84 91     91 1 526 tie my %hash, 'Mango::BSON::Document', @_;
85 91         409 return \%hash;
86             }
87              
88 3     3 1 17 sub bson_double { Mango::BSON::Number->new(shift, DOUBLE) }
89              
90             sub bson_encode {
91 113     113 1 8122 my $doc = shift;
92              
93             # Embedded BSON
94 113 100       327 return $doc->{'$bson'} if exists $doc->{'$bson'};
95              
96             my $bson = join '',
97 110         337 map { _encode_value(encode_cstring($_), $doc->{$_}) } keys %$doc;
  114         225  
98              
99             # Document ends with null byte
100 110         797 return pack('l<', length($bson) + 5) . $bson . "\x00";
101             }
102              
103 10     10 1 43 sub bson_false {$FALSE}
104              
105 7     7 1 461 sub bson_int32 { Mango::BSON::Number->new(shift, INT32) }
106              
107 7     7 1 492 sub bson_int64 { Mango::BSON::Number->new(shift, INT64) }
108              
109 73 100   73 1 384 sub bson_length { length $_[0] < 4 ? undef : unpack 'l<', substr($_[0], 0, 4) }
110              
111 2     2 1 11 sub bson_max {$MAXKEY}
112              
113 2     2 1 11 sub bson_min {$MINKEY}
114              
115 13     13 1 3724 sub bson_oid { Mango::BSON::ObjectID->new(@_) }
116              
117 1     1 1 3 sub bson_raw { bson_doc('$bson' => shift) }
118              
119 14     14 1 2385 sub bson_time { Mango::BSON::Time->new(@_) }
120              
121             sub bson_ts {
122 1     1 1 20 Mango::BSON::Timestamp->new(seconds => shift, increment => shift);
123             }
124              
125 10     10 1 44 sub bson_true {$TRUE}
126              
127             sub encode_cstring {
128 120     120 1 203 my $str = shift;
129 120         273 utf8::encode $str;
130 120         524 return pack 'Z*', $str;
131             }
132              
133             sub _decode_binary {
134 6     6   10 my $bsonref = shift;
135              
136 6         23 my $len = unpack 'l<', substr($$bsonref, 0, 4, '');
137 6         16 my $subtype = substr $$bsonref, 0, 1, '';
138 6         13 my $binary = substr $$bsonref, 0, $len, '';
139              
140 6 100       20 return bson_bin($binary)->type('function') if $subtype eq BINARY_FUNCTION;
141 5 100       19 return bson_bin($binary)->type('md5') if $subtype eq BINARY_MD5;
142 4 100       11 return bson_bin($binary)->type('uuid') if $subtype eq BINARY_UUID;
143 3 100       11 return bson_bin($binary)->type('user_defined')
144             if $subtype eq BINARY_USER_DEFINED;
145 2         9 return bson_bin($binary)->type('generic');
146             }
147              
148             sub _decode_cstring {
149 77     77   88 my $bsonref = shift;
150 77         154 my $str = substr $$bsonref, 0, index($$bsonref, "\x00"), '';
151 77         185 utf8::decode $str;
152 77         105 substr $$bsonref, 0, 1, '';
153 77         162 return $str;
154             }
155              
156             sub _decode_doc {
157 75     75   110 my $bsonref = shift;
158              
159             # Every element starts with a type
160 75         80 my @doc;
161 75         131 substr $$bsonref, 0, 4, '';
162 75         180 while (my $type = substr $$bsonref, 0, 1, '') {
163              
164             # Null byte (end of document)
165 150 100       441 last if $type eq "\x00";
166              
167 75         123 push @doc, _decode_cstring($bsonref), _decode_value($type, $bsonref);
168             }
169              
170 75         134 return bson_doc(@doc);
171             }
172              
173             sub _decode_string {
174 17     17   21 my $bsonref = shift;
175              
176 17         37 my $len = unpack 'l<', substr($$bsonref, 0, 4, '');
177 17         42 my $str = substr $$bsonref, 0, $len - 1, '';
178 17         36 utf8::decode $str;
179 17         21 substr $$bsonref, 0, 1, '';
180              
181 17         54 return $str;
182             }
183              
184             sub _decode_value {
185 75     75   135 my ($type, $bsonref) = @_;
186              
187             # String
188 75 100       137 return _decode_string($bsonref) if $type eq STRING;
189              
190             # Object ID
191 60 100       116 return bson_oid(unpack 'H*', substr $$bsonref, 0, 12, '')
192             if $type eq OBJECT_ID;
193              
194             # Double/Int32/Int64
195 58 100       123 return unpack 'd<', substr $$bsonref, 0, 8, '' if $type eq DOUBLE;
196 51 100       110 return unpack 'l<', substr($$bsonref, 0, 4, '') if $type eq INT32;
197 43 100       83 return unpack 'q<', substr($$bsonref, 0, 8, '') if $type eq INT64;
198              
199             # Document
200 41 100       77 return _decode_doc($bsonref) if $type eq DOCUMENT;
201              
202             # Array
203 31 100       57 return [values %{_decode_doc($bsonref)}] if $type eq ARRAY;
  6         15  
204              
205             # Booleans and Null
206 25 100       74 return substr($$bsonref, 0, 1, '') eq "\x00" ? bson_false() : bson_true()
    100          
207             if $type eq BOOL;
208 15 100       26 return undef if $type eq NULL;
209              
210             # Time
211 14 100       37 return bson_time(unpack 'q<', substr($$bsonref, 0, 8, ''))
212             if $type eq DATETIME;
213              
214             # Regex
215 12 100       25 if ($type eq REGEX) {
216 1         3 my ($p, $m) = (_decode_cstring($bsonref), _decode_cstring($bsonref));
217 1 50 33     14 croak "invalid regex modifier(s) in 'qr/$p/$m'"
218             if length($m) and $m !~ /^[msixpadlun]+\z/;
219             # escape $pat to avoid code injection
220 1         62 return eval "qr/\$p/$m";
221             }
222              
223             # Binary (with subtypes)
224 11 100       31 return _decode_binary($bsonref) if $type eq BINARY;
225              
226             # Min/Max
227 5 100       15 return bson_min() if $type eq MIN_KEY;
228 4 100       11 return bson_max() if $type eq MAX_KEY;
229              
230             # Code (with and without scope)
231 3 100       10 return bson_code(_decode_string($bsonref)) if $type eq CODE;
232 2 100       6 if ($type eq CODE_SCOPE) {
233 1         3 substr $$bsonref, 0, 4, '';
234 1         3 return bson_code(_decode_string($bsonref))->scope(_decode_doc($bsonref));
235             }
236              
237             # Timestamp
238             return bson_ts(
239 1 50       4 reverse map({ unpack 'l<', substr($$_, 0, 4, '') } $bsonref, $bsonref))
  2         9  
240             if $type eq TIMESTAMP;
241              
242             # Undefined - a deprecated type which should not exist anymore
243             # but apparently still does: https://github.com/oliwer/mango/issues/1
244 0 0       0 return undef if $type eq UNDEFINED;
245              
246             # Unknown
247 0         0 croak 'Unknown BSON type';
248             }
249              
250             sub _encode_binary {
251 6     6   15 my ($e, $subtype, $value) = @_;
252 6         42 return BINARY . $e . pack('l<', length $value) . $subtype . $value;
253             }
254              
255             sub _encode_object {
256 48     48   94 my ($e, $value, $class) = @_;
257              
258             # ObjectID
259 48 100       91 return OBJECT_ID . $e . $value->to_bytes
260             if $class eq 'Mango::BSON::ObjectID';
261              
262             # Boolean
263 46 100       123 return BOOL . $e . ($value ? "\x01" : "\x00") if $class eq $BOOL;
    100          
264              
265             # Time
266 36 100       61 return DATETIME . $e . pack('q<', $value) if $class eq 'Mango::BSON::Time';
267              
268             # Max
269 34 100       164 return MAX_KEY . $e if $value eq $MAXKEY;
270              
271             # Min
272 33 100       191 return MIN_KEY . $e if $value eq $MINKEY;
273              
274             # Regex
275 32 100       129 if ($class eq 'Regexp') {
276 1         6 my ($p, $m) = regexp_pattern($value);
277 1         4 return REGEX . $e . encode_cstring($p) . encode_cstring($m);
278             }
279              
280             # Binary
281 31 100       57 if ($class eq 'Mango::BSON::Binary') {
282 6   50     14 my $type = $value->type // 'generic';
283 6         27 my $data = $value->data;
284 6 100       25 return _encode_binary($e, BINARY_FUNCTION, $data) if $type eq 'function';
285 5 100       12 return _encode_binary($e, BINARY_MD5, $data) if $type eq 'md5';
286 4 100       12 return _encode_binary($e, BINARY_USER_DEFINED, $data)
287             if $type eq 'user_defined';
288 3 100       9 return _encode_binary($e, BINARY_UUID, $data) if $type eq 'uuid';
289 2         9 return _encode_binary($e, BINARY_GENERIC, $data);
290             }
291              
292             # Code
293 25 100       41 if ($class eq 'Mango::BSON::Code') {
294              
295             # With scope
296 2 100       8 if (my $scope = $value->scope) {
297 1         9 my $code = _encode_string($value->code) . bson_encode($scope);
298 1         8 return CODE_SCOPE . $e . pack('l<', length $code) . $code;
299             }
300              
301             # Without scope
302 1         12 return CODE . $e . _encode_string($value->code);
303             }
304              
305             # Timestamp
306 23 100       49 return TIMESTAMP, $e, map { pack 'l<', $_ } $value->increment,
  2         13  
307             $value->seconds
308             if $class eq 'Mango::BSON::Timestamp';
309              
310             # Number
311 22 100       39 if ($class eq 'Mango::BSON::Number') {
312 17         29 my $t = $value->type;
313 17         65 return $t . $e . pack($num_pack_fmt{$t}, $value->value);
314             }
315              
316             # Blessed reference with TO_JSON method
317 5 100 100     49 if (my $sub = $value->can('TO_BSON') // $value->can('TO_JSON')) {
318 4         17 return _encode_value($e, $value->$sub);
319             }
320              
321             # Stringify
322 1         5 return STRING . $e . _encode_string($value);
323             }
324              
325             sub _encode_string {
326 27     27   54 my $str = shift;
327 27         60 utf8::encode $str;
328 27         193 return pack('l<', length($str) + 1) . "$str\x00";
329             }
330              
331             sub _encode_value {
332 118     118   355 my ($e, $value) = @_;
333              
334             # Null
335 118 100       248 return NULL . $e unless defined $value;
336              
337             # Reference
338 117 100       269 if (my $ref = ref $value) {
339              
340             # Blessed
341 70 100       238 return _encode_object($e, $value, $ref) if blessed $value;
342              
343             # Hash (Document)
344 28 100       74 return DOCUMENT . $e . bson_encode($value) if $ref eq 'HASH';
345              
346             # Array
347 17 100       36 if ($ref eq 'ARRAY') {
348 11         20 my $i = 0;
349 11         34 return ARRAY . $e . bson_encode(bson_doc(map { $i++ => $_ } @$value));
  26         78  
350             }
351              
352             # Scalar (boolean shortcut)
353 6 50       18 return _encode_object($e, !!$$value, $BOOL) if $ref eq 'SCALAR';
354             }
355              
356             # Numeric
357 47 100       123 if (my $type = Mango::BSON::Number::guess_type($value)) {
358 23         149 return $type . $e . pack($num_pack_fmt{$type}, $value);
359             }
360              
361             # String
362 24         86 return STRING . $e . _encode_string("$value");
363             }
364              
365             # Constants
366             package Mango::BSON::_MaxKey;
367              
368             package Mango::BSON::_MinKey;
369              
370             1;
371              
372             =encoding utf8
373              
374             =head1 NAME
375              
376             Mango::BSON - BSON
377              
378             =head1 SYNOPSIS
379              
380             use Mango::BSON ':bson';
381              
382             my $bson = bson_encode {
383             foo => 'bar',
384             baz => 0.42,
385             unordered => {one => [1, 2, 3], two => bson_time},
386             ordered => bson_doc(one => qr/test/i, two => bson_true)
387             };
388             my $doc = bson_decode $bson;
389              
390             =head1 DESCRIPTION
391              
392             L is a minimalistic implementation of L.
393              
394             In addition to a bunch of custom BSON data types it supports normal Perl data
395             types like scalar, regular expression, C, array reference, hash
396             reference and will try to call the C and C methods on
397             blessed references, or stringify them if it doesn't exist. Scalar references
398             will be used to generate booleans, based on if their values are true or false.
399              
400             =head1 FUNCTIONS
401              
402             L implements the following functions, which can be imported
403             individually or at once with the C<:bson> flag.
404              
405             =head2 bson_bin
406              
407             my $bin = bson_bin $bytes;
408              
409             Create new BSON element of the binary type with L,
410             defaults to the C binary subtype.
411              
412             # Function
413             bson_bin($bytes)->type('function');
414              
415             # MD5
416             bson_bin($bytes)->type('md5');
417              
418             # UUID
419             bson_bin($bytes)->type('uuid');
420              
421             # User defined
422             bson_bin($bytes)->type('user_defined');
423              
424             =head2 bson_code
425              
426             my $code = bson_code 'function () {}';
427              
428             Create new BSON element of the code type with L.
429              
430             # With scope
431             bson_code('function () {}')->scope({foo => 'bar'});
432              
433             =head2 bson_dbref
434              
435             my $dbref = bson_dbref 'test', $oid;
436              
437             Create a new database reference.
438              
439             # Longer version
440             my $dbref = {'$ref' => 'test', '$id' => $oid};
441              
442             =head2 bson_decode
443              
444             my $doc = bson_decode $bson;
445              
446             Decode BSON into Perl data structures.
447              
448             =head2 bson_doc
449              
450             my $doc = bson_doc;
451             my $doc = bson_doc foo => 'bar', baz => 0.42, yada => {yada => [1, 2, 3]};
452              
453             Create new BSON document with L, which can also be used
454             as a generic ordered hash.
455              
456             # Order is preserved
457             my $hash = bson_doc one => 1, two => 2, three => 3;
458             $hash->{four} = 4;
459             delete $hash->{two};
460             say for keys %$hash;
461              
462             =head2 bson_double
463              
464             my $doc = { foo => bson_double(13.0) };
465              
466             Force a scalar value to be encoded as a double in MongoDB. Croaks if the
467             value is incompatible with the double type.
468              
469             =head2 bson_encode
470              
471             my $bson = bson_encode $doc;
472             my $bson = bson_encode {};
473              
474             Encode Perl data structures into BSON.
475              
476             =head2 bson_false
477              
478             my $false = bson_false;
479              
480             Create new BSON element of the boolean type false.
481              
482             =head2 bson_int32
483              
484             my $doc = { foo => bson_int32(13) };
485              
486             # This will die (integer is too big)
487             my $doc = { foo => bson_int32(2147483648) };
488              
489             Force a scalar value to be encoded as a 32 bit integer in MongoDB. Croaks if
490             the value is incompatible with the int32 type.
491              
492             =head2 bson_int64
493              
494             my $doc = { foo => bson_int64(666) };
495              
496             Force a scalar value to be encoded as a 64 bit integer in MongoDB. Croaks if
497             the value is incompatible with the int64 type.
498              
499             =head2 bson_length
500              
501             my $len = bson_length $bson;
502              
503             Check BSON length prefix.
504              
505             =head2 bson_max
506              
507             my $max_key = bson_max;
508              
509             Create new BSON element of the max key type.
510              
511             =head2 bson_min
512              
513             my $min_key = bson_min;
514              
515             Create new BSON element of the min key type.
516              
517             =head2 bson_oid
518              
519             my $oid = bson_oid;
520             my $oid = bson_oid '1a2b3c4e5f60718293a4b5c6';
521              
522             Create new BSON element of the object id type with L,
523             defaults to generating a new unique object id.
524              
525             # Generate object id with specific epoch time
526             my $oid = bson_oid->from_epoch(1359840145);
527              
528             =head2 bson_raw
529              
530             my $raw = bson_raw $bson;
531              
532             Pre-encoded BSON document.
533              
534             # Longer version
535             my $raw = {'$bson' => $bson};
536              
537             # Embed pre-encoded BSON document
538             my $first = bson_encode {foo => 'bar'};
539             my $second = bson_encode {test => bson_raw $first};
540              
541             =head2 bson_time
542              
543             my $now = bson_time;
544             my $time = bson_time time * 1000;
545              
546             Create new BSON element of the UTC datetime type with L,
547             defaults to milliseconds since the UNIX epoch.
548              
549             # "1360626536.748"
550             bson_time(1360626536748)->to_epoch;
551              
552             # "2013-02-11T23:48:56.748Z"
553             bson_time(1360626536748)->to_datetime;
554              
555             =head2 bson_true
556              
557             my $true = bson_true;
558              
559             Create new BSON element of the boolean type true.
560              
561             =head2 bson_ts
562              
563             my $timestamp = bson_ts 23, 24;
564              
565             Create new BSON element of the timestamp type with L.
566              
567             =head2 encode_cstring
568              
569             my $bytes = encode_cstring $cstring;
570              
571             Encode cstring.
572              
573             =head1 SEE ALSO
574              
575             L, L, L.
576              
577             =cut