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   20403 use Mojo::Base -strict;
  11         12  
  11         60  
3              
4 11     11   935 use re 'regexp_pattern';
  11         11  
  11         1342  
5 11     11   44 use Carp 'croak';
  11         12  
  11         509  
6 11     11   37 use Exporter 'import';
  11         10  
  11         270  
7 11     11   3455 use Mango::BSON::Binary;
  11         22  
  11         103  
8 11     11   4415 use Mango::BSON::Code;
  11         19  
  11         92  
9 11     11   3567 use Mango::BSON::Document;
  11         19  
  11         278  
10 11     11   3684 use Mango::BSON::Number;
  11         18  
  11         66  
11 11     11   3816 use Mango::BSON::ObjectID;
  11         17  
  11         192  
12 11     11   3703 use Mango::BSON::Time;
  11         19  
  11         74  
13 11     11   3719 use Mango::BSON::Timestamp;
  11         20  
  11         58  
14 11     11   4512 use Mojo::JSON;
  11         133412  
  11         489  
15 11     11   63 use Scalar::Util 'blessed';
  11         13  
  11         1671  
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         2480 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   47 };
  11         13  
46              
47             # Binary subtypes
48             use constant {
49 11         21010 BINARY_GENERIC => "\x00",
50             BINARY_FUNCTION => "\x01",
51             BINARY_UUID => "\x04",
52             BINARY_MD5 => "\x05",
53             BINARY_USER_DEFINED => "\x80"
54 11     11   50 };
  11         12  
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 2642 sub bson_bin { Mango::BSON::Binary->new(data => shift) }
72              
73 4     4 1 639 sub bson_code { Mango::BSON::Code->new(code => shift) }
74              
75 1     1 1 9 sub bson_dbref { bson_doc('$ref' => shift, '$id' => shift) }
76              
77             sub bson_decode {
78 62     62 1 852 my $bson = shift;
79 62 100       78 return undef unless my $len = bson_length($bson);
80 59 100       135 return length $bson == $len ? _decode_doc(\$bson) : undef;
81             }
82              
83             sub bson_doc {
84 91     91 1 382 tie my %hash, 'Mango::BSON::Document', @_;
85 91         259 return \%hash;
86             }
87              
88 3     3 1 15 sub bson_double { Mango::BSON::Number->new(shift, DOUBLE) }
89              
90             sub bson_encode {
91 113     113 1 7981 my $doc = shift;
92              
93             # Embedded BSON
94 113 100       280 return $doc->{'$bson'} if exists $doc->{'$bson'};
95              
96             my $bson = join '',
97 110         263 map { _encode_value(encode_cstring($_), $doc->{$_}) } keys %$doc;
  114         141  
98              
99             # Document ends with null byte
100 110         568 return pack('l<', length($bson) + 5) . $bson . "\x00";
101             }
102              
103 10     10 1 31 sub bson_false {$FALSE}
104              
105 7     7 1 388 sub bson_int32 { Mango::BSON::Number->new(shift, INT32) }
106              
107 7     7 1 358 sub bson_int64 { Mango::BSON::Number->new(shift, INT64) }
108              
109 73 100   73 1 333 sub bson_length { length $_[0] < 4 ? undef : unpack 'l<', substr($_[0], 0, 4) }
110              
111 2     2 1 8 sub bson_max {$MAXKEY}
112              
113 2     2 1 9 sub bson_min {$MINKEY}
114              
115 13     13 1 3055 sub bson_oid { Mango::BSON::ObjectID->new(@_) }
116              
117 1     1 1 3 sub bson_raw { bson_doc('$bson' => shift) }
118              
119 12     12 1 1055 sub bson_time { Mango::BSON::Time->new(@_) }
120              
121             sub bson_ts {
122 1     1 1 14 Mango::BSON::Timestamp->new(seconds => shift, increment => shift);
123             }
124              
125 10     10 1 30 sub bson_true {$TRUE}
126              
127             sub encode_cstring {
128 120     120 1 95 my $str = shift;
129 120         162 utf8::encode $str;
130 120         375 return pack 'Z*', $str;
131             }
132              
133             sub _decode_binary {
134 6     6   6 my $bsonref = shift;
135              
136 6         7 my $len = unpack 'l<', substr($$bsonref, 0, 4, '');
137 6         7 my $subtype = substr $$bsonref, 0, 1, '';
138 6         7 my $binary = substr $$bsonref, 0, $len, '';
139              
140 6 100       11 return bson_bin($binary)->type('function') if $subtype eq BINARY_FUNCTION;
141 5 100       10 return bson_bin($binary)->type('md5') if $subtype eq BINARY_MD5;
142 4 100       5 return bson_bin($binary)->type('uuid') if $subtype eq BINARY_UUID;
143 3 100       7 return bson_bin($binary)->type('user_defined')
144             if $subtype eq BINARY_USER_DEFINED;
145 2         4 return bson_bin($binary)->type('generic');
146             }
147              
148             sub _decode_cstring {
149 77     77   59 my $bsonref = shift;
150 77         126 my $str = substr $$bsonref, 0, index($$bsonref, "\x00"), '';
151 77         99 utf8::decode $str;
152 77         65 substr $$bsonref, 0, 1, '';
153 77         137 return $str;
154             }
155              
156             sub _decode_doc {
157 75     75   72 my $bsonref = shift;
158              
159             # Every element starts with a type
160 75         62 my @doc;
161 75         84 substr $$bsonref, 0, 4, '';
162 75         139 while (my $type = substr $$bsonref, 0, 1, '') {
163              
164             # Null byte (end of document)
165 150 100       359 last if $type eq "\x00";
166              
167 75         105 push @doc, _decode_cstring($bsonref), _decode_value($type, $bsonref);
168             }
169              
170 75         87 return bson_doc(@doc);
171             }
172              
173             sub _decode_string {
174 17     17   14 my $bsonref = shift;
175              
176 17         26 my $len = unpack 'l<', substr($$bsonref, 0, 4, '');
177 17         28 my $str = substr $$bsonref, 0, $len - 1, '';
178 17         20 utf8::decode $str;
179 17         13 substr $$bsonref, 0, 1, '';
180              
181 17         49 return $str;
182             }
183              
184             sub _decode_value {
185 75     75   75 my ($type, $bsonref) = @_;
186              
187             # String
188 75 100       122 return _decode_string($bsonref) if $type eq STRING;
189              
190             # Object ID
191 60 100       89 return bson_oid(unpack 'H*', substr $$bsonref, 0, 12, '')
192             if $type eq OBJECT_ID;
193              
194             # Double/Int32/Int64
195 58 100       103 return unpack 'd<', substr $$bsonref, 0, 8, '' if $type eq DOUBLE;
196 51 100       91 return unpack 'l<', substr($$bsonref, 0, 4, '') if $type eq INT32;
197 43 100       58 return unpack 'q<', substr($$bsonref, 0, 8, '') if $type eq INT64;
198              
199             # Document
200 41 100       59 return _decode_doc($bsonref) if $type eq DOCUMENT;
201              
202             # Array
203 31 100       47 return [values %{_decode_doc($bsonref)}] if $type eq ARRAY;
  6         7  
204              
205             # Booleans and Null
206 25 100       53 return substr($$bsonref, 0, 1, '') eq "\x00" ? bson_false() : bson_true()
    100          
207             if $type eq BOOL;
208 15 100       22 return undef if $type eq NULL;
209              
210             # Time
211 14 100       24 return bson_time(unpack 'q<', substr($$bsonref, 0, 8, ''))
212             if $type eq DATETIME;
213              
214             # Regex
215 12 100       13 if ($type eq REGEX) {
216 1         2 my ($p, $m) = (_decode_cstring($bsonref), _decode_cstring($bsonref));
217 1 50 33     12 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         80 return eval "qr/\$p/$m";
221             }
222              
223             # Binary (with subtypes)
224 11 100       22 return _decode_binary($bsonref) if $type eq BINARY;
225              
226             # Min/Max
227 5 100       10 return bson_min() if $type eq MIN_KEY;
228 4 100       6 return bson_max() if $type eq MAX_KEY;
229              
230             # Code (with and without scope)
231 3 100       5 return bson_code(_decode_string($bsonref)) if $type eq CODE;
232 2 100       4 if ($type eq CODE_SCOPE) {
233 1         2 substr $$bsonref, 0, 4, '';
234 1         2 return bson_code(_decode_string($bsonref))->scope(_decode_doc($bsonref));
235             }
236              
237             # Timestamp
238             return bson_ts(
239 1 50       3 reverse map({ unpack 'l<', substr($$_, 0, 4, '') } $bsonref, $bsonref))
  2         7  
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   7 my ($e, $subtype, $value) = @_;
252 6         34 return BINARY . $e . pack('l<', length $value) . $subtype . $value;
253             }
254              
255             sub _encode_object {
256 48     48   49 my ($e, $value, $class) = @_;
257              
258             # ObjectID
259 48 100       98 return OBJECT_ID . $e . $value->to_bytes
260             if $class eq 'Mango::BSON::ObjectID';
261              
262             # Boolean
263 46 100       83 return BOOL . $e . ($value ? "\x01" : "\x00") if $class eq $BOOL;
    100          
264              
265             # Time
266 36 100       44 return DATETIME . $e . pack('q<', $value) if $class eq 'Mango::BSON::Time';
267              
268             # Max
269 34 100       157 return MAX_KEY . $e if $value eq $MAXKEY;
270              
271             # Min
272 33 100       161 return MIN_KEY . $e if $value eq $MINKEY;
273              
274             # Regex
275 32 100       116 if ($class eq 'Regexp') {
276 1         3 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       43 if ($class eq 'Mango::BSON::Binary') {
282 6   50     11 my $type = $value->type // 'generic';
283 6         23 my $data = $value->data;
284 6 100       19 return _encode_binary($e, BINARY_FUNCTION, $data) if $type eq 'function';
285 5 100       8 return _encode_binary($e, BINARY_MD5, $data) if $type eq 'md5';
286 4 100       6 return _encode_binary($e, BINARY_USER_DEFINED, $data)
287             if $type eq 'user_defined';
288 3 100       7 return _encode_binary($e, BINARY_UUID, $data) if $type eq 'uuid';
289 2         4 return _encode_binary($e, BINARY_GENERIC, $data);
290             }
291              
292             # Code
293 25 100       29 if ($class eq 'Mango::BSON::Code') {
294              
295             # With scope
296 2 100       6 if (my $scope = $value->scope) {
297 1         6 my $code = _encode_string($value->code) . bson_encode($scope);
298 1         6 return CODE_SCOPE . $e . pack('l<', length $code) . $code;
299             }
300              
301             # Without scope
302 1         8 return CODE . $e . _encode_string($value->code);
303             }
304              
305             # Timestamp
306 23 100       31 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       33 if ($class eq 'Mango::BSON::Number') {
312 17         25 my $t = $value->type;
313 17         62 return $t . $e . pack($num_pack_fmt{$t}, $value->value);
314             }
315              
316             # Blessed reference with TO_JSON method
317 5 100 100     40 if (my $sub = $value->can('TO_BSON') // $value->can('TO_JSON')) {
318 4         8 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   25 my $str = shift;
327 27         29 utf8::encode $str;
328 27         113 return pack('l<', length($str) + 1) . "$str\x00";
329             }
330              
331             sub _encode_value {
332 118     118   207 my ($e, $value) = @_;
333              
334             # Null
335 118 100       181 return NULL . $e unless defined $value;
336              
337             # Reference
338 117 100       182 if (my $ref = ref $value) {
339              
340             # Blessed
341 70 100       208 return _encode_object($e, $value, $ref) if blessed $value;
342              
343             # Hash (Document)
344 28 100       60 return DOCUMENT . $e . bson_encode($value) if $ref eq 'HASH';
345              
346             # Array
347 17 100       26 if ($ref eq 'ARRAY') {
348 11         10 my $i = 0;
349 11         19 return ARRAY . $e . bson_encode(bson_doc(map { $i++ => $_ } @$value));
  26         42  
350             }
351              
352             # Scalar (boolean shortcut)
353 6 50       14 return _encode_object($e, !!$$value, $BOOL) if $ref eq 'SCALAR';
354             }
355              
356             # Numeric
357 47 100       72 if (my $type = Mango::BSON::Number::guess_type($value)) {
358 23         101 return $type . $e . pack($num_pack_fmt{$type}, $value);
359             }
360              
361             # String
362 24         49 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