File Coverage

blib/lib/Mongoose/Engine.pm
Criterion Covered Total %
statement 18 229 7.8
branch 0 152 0.0
condition 0 43 0.0
subroutine 6 30 20.0
pod 14 14 100.0
total 38 468 8.1


line stmt bran cond sub pod time code
1             package Mongoose::Engine;
2             $Mongoose::Engine::VERSION = '2.02';
3 1     1   703 use Moose::Role;
  1         2  
  1         7  
4              
5 1     1   4591 use Carp;
  1         2  
  1         61  
6 1     1   5 use Scalar::Util qw/refaddr reftype/;
  1         2  
  1         50  
7 1     1   6 use List::Util qw/first/;
  1         2  
  1         47  
8 1     1   5 use boolean;
  1         2  
  1         8  
9              
10 1     1   418 use Mongoose::Cursor; #initializes moose
  1         388  
  1         2532  
11              
12             with 'Mongoose::Role::Collapser';
13             with 'Mongoose::Role::Expander';
14             with 'Mongoose::Role::Engine';
15              
16             sub collapse {
17 0     0 1   my ($self, @scope) = @_;
18              
19             # circularity ?
20 0 0   0     if( my $duplicate = first { refaddr($self) == refaddr($_) } @scope ) {
  0            
21 0           my $class = blessed $duplicate;
22 0           my $ref_id = $duplicate->_id;
23 0 0 0       return undef unless defined $class && $ref_id;
24 0 0 0       return undef if $self->_id && $self->_id eq $ref_id; # prevent self references?
25             return BSON::DBRef->new(
26             ref => Mongoose->class_config($class)->{collection_name},
27 0           id => $ref_id
28             );
29             }
30              
31 0           my $packed = { %$self }; # cheesely clone the data
32 0           for my $key ( keys %$packed ) {
33             # treat special cases based on Moose attribute defs or traits
34 0 0         if ( my $attr = $self->meta->get_attribute($key) ) {
35 0 0         delete $packed->{$key},
36             next if $attr->does('Mongoose::Meta::Attribute::Trait::DoNotMongoSerialize');
37              
38 0 0         next if $attr->does('Mongoose::Meta::Attribute::Trait::Raw');
39              
40 0 0         if ( my $type = $attr->type_constraint ) {
41 0 0         if ( $type->is_a_type_of('Num') ) {
    0          
42 0           $packed->{$key} += 0; # Ensure it's saved as a number
43 0           next;
44             }
45             elsif ( $type->is_a_type_of('FileHandle') ) {
46             $packed->{$key} = BSON::DBRef->new(
47             ref => 'FileHandle',
48 0           id => $self->db->gfs->upload_from_stream( $key. time, delete($packed->{$key}) )
49             );
50 0           next;
51             }
52             }
53             }
54              
55 0           $packed->{$key} = $self->_collapse( $packed->{$key}, @scope );
56             }
57              
58 0           $packed;
59             }
60              
61             sub _collapse {
62 0     0     my ($self, $value, @scope ) = @_;
63              
64 0 0         if ( my $class = blessed $value ) {
    0          
    0          
65 0 0 0       if ( ref $value eq 'HASH' && defined ( my $ref_id = $value->{_id} ) ) {
66             # it has an id, so join ref it
67             return BSON::DBRef->new(
68             ref => Mongoose->class_config($class)->{collection_name},
69 0           id => $ref_id
70             );
71             }
72              
73 0           return $self->_unbless( $value, $class, @scope );
74             }
75             elsif ( ref $value eq 'ARRAY' ) {
76 0           my @arr;
77 0           for my $item ( @$value ) {
78 0   0       my $aryclass ||= blessed( $item );
79 0 0 0       if ( $aryclass && $aryclass->does('Mongoose::EmbeddedDocument') ) {
    0 0        
80 0           push @arr, $item->collapse(@scope, $self);
81             }
82             elsif ( $aryclass && $aryclass->does('Mongoose::Document') ) {
83 0           $item->_save( @scope, $self );
84             push @arr, BSON::DBRef->new(
85             ref => Mongoose->class_config($aryclass)->{collection_name},
86 0           id => $item->_id
87             );
88             }
89             else {
90 0           push @arr, $item;
91             }
92             }
93 0           return \@arr;
94             }
95             elsif ( ref $value eq 'HASH' ) {
96 0           my $ret = {};
97 0           my @docs;
98 0           for my $key ( keys %$value ) {
99 0 0         if ( blessed $value->{$key} ) {
    0          
100 0           $ret->{$key} = $self->_unbless( $value->{$key}, blessed($value->{$key}), @scope );
101             }
102             elsif ( ref $value->{$key} eq 'ARRAY' ) {
103 0           $ret->{$key} = [ map { $self->_collapse( $_, @scope ) } @{ $value->{$key} } ];
  0            
  0            
104             }
105             else {
106 0           $ret->{$key} = $value->{$key};
107             }
108             }
109 0           return $ret;
110             }
111              
112 0           $value;
113             }
114              
115             sub _unbless {
116 0     0     my ($self, $obj, $class, @scope ) = @_;
117              
118 0           my $ret = $obj;
119 0 0         if ( $class->can('meta') ) { # only mooses from here on
    0          
120 0 0         if ( $class->does('Mongoose::EmbeddedDocument') ) {
    0          
    0          
121 0 0         $ret = $obj->collapse( @scope, $self ) or next;
122             }
123             elsif ( $class->does('Mongoose::Document') ) {
124 0           $obj->_save( @scope, $self );
125             $ret = BSON::DBRef->new(
126             ref => Mongoose->class_config($class)->{collection_name},
127 0           id => $obj->_id
128             );
129             }
130             elsif ( $class->isa('Mongoose::Join') ) {
131 0           my @objs = $obj->_save( $self, @scope );
132 0           $ret = \@objs;
133             }
134             }
135             # non-moose class
136             elsif ( $class !~ /^(?: DateTime(?:\:\:Tiny)? | boolean | ^BSON::)$/x ) { # Types accepted by the driver
137 0           my $reftype = reftype($obj);
138 0 0         if ( $reftype eq 'ARRAY' ) { $ret = [@$obj] }
  0 0          
    0          
139 0           elsif ( $reftype eq 'SCALAR' ) { $ret = $$obj }
140 0           elsif ( $reftype eq 'HASH' ) { $ret = {%$obj} }
141             }
142              
143 0           $ret;
144             }
145              
146             sub _expand_subtype {
147 0     0     my ( $self, $param_class, $value, $scope ) = @_;
148              
149 0 0         return $value->as_datetime if $param_class eq 'DateTime';
150              
151 0 0         return $param_class->expand($value) if $param_class->does('Mongoose::EmbeddedDocument');
152              
153 0 0         if ( $param_class->does('Mongoose::Document') ) {
154 0 0         if ( my $circ_doc = $scope->{ $value->id } ) {
    0          
155 0           return bless( $circ_doc, $param_class );
156             }
157             elsif ( my $obj = $param_class->find_one({ _id => $value->id }, undef, $scope ) ) {
158 0           return $obj;
159             }
160             }
161              
162 0   0       warn "Unable to expand subtype $param_class from ". ref($value) || "unblesed value: $value";
163 0           $value; # Not expanded :-/
164             }
165              
166             sub expand {
167 0     0 1   my ( $self, $doc, $fields, $scope ) = @_;
168 0           my $config = Mongoose->class_config($self);
169 0   0       my $class_main = ref $self || $self;
170 0           my @later;
171              
172 0 0         $scope = {} unless ref $scope eq 'HASH';
173              
174             # check if it's an straight ref
175 0 0         if ( ref $doc eq 'BSON::DBRef' ) {
176             return defined $scope->{$doc->id}
177 0 0         ? $scope->{$doc->id}
178             : $class_main->find_one({ _id => $doc->id });
179             #TODO: set at $scope?
180             }
181              
182 0           for my $attr ( $class_main->meta->get_all_attributes ) {
183 0           my $name = $attr->name;
184              
185 0 0         next unless exists $doc->{$name};
186 0 0         next if $attr->does('Mongoose::Meta::Attribute::Trait::Raw');
187              
188 0 0         my $type = $attr->type_constraint or next;
189 0 0         my $class = $self->_get_blessed_type( $type ) or next;
190              
191 0 0         if ( $type->is_a_type_of('HashRef') ) {
    0          
    0          
    0          
192             # HashRef[ parameter ]
193 0 0         if( defined $type->{type_parameter} ) {
194 0           my $param = $type->{type_parameter};
195 0 0         if ( my $param_class = $param->{class} ) {
196 0 0         for my $key ( keys %{ $doc->{$name} || {} } ) {
  0            
197 0           $doc->{$name}{$key} = $self->_expand_subtype( $param_class, $doc->{$name}{$key}, $scope );
198             }
199             }
200             else {
201 0   0       $doc->{$name} ||= {};
202             }
203             }
204              
205 0           next;
206             }
207             elsif ( $type->is_a_type_of('ArrayRef') ) {
208 0 0         if ( defined $type->{type_parameter} ) {
209             # ArrayRef[ parameter ]
210 0           my $param = $type->{type_parameter};
211 0 0         if ( my $param_class = $param->{class} ) {
212 0           my @objs;
213 0 0         for my $item ( @{ $doc->{$name} || [] } ) {
  0            
214 0           push @objs, $self->_expand_subtype( $param_class, $item, $scope );
215             }
216 0           $doc->{$name} = \@objs;
217             }
218             else {
219 0   0       $doc->{$name} ||= [];
220             }
221             }
222              
223 0           next;
224             }
225             elsif ( $type->is_a_type_of('DateTime') ) {
226 0           $doc->{$name} = $doc->{$name}->as_datetime,
227             next;
228             }
229             elsif( $type->is_a_type_of('FileHandle') ) {
230             $doc->{$name} = Mongoose::File->new(
231 0           file_id => $doc->{$name}->id,
232             bucket => $self->db->gfs
233             );
234 0           next;
235             }
236              
237 0 0         if( $class->can('meta') ) { # moose subobject
238              
239 0 0         if ( $class->does('Mongoose::EmbeddedDocument') ) {
    0          
    0          
240 0           $doc->{$name} = bless $doc->{$name}, $class;
241             }
242             elsif ( $class->does('Mongoose::Document') ) {
243 0 0         if ( ref $doc->{$name} eq 'BSON::DBRef' ) {
244 0           my $_id = $doc->{$name}->id;
245 0 0         if ( my $circ_doc = $scope->{"$_id"} ) {
246 0           $doc->{$name} = bless( $circ_doc , $class );
247 0           $scope->{ "$circ_doc->{_id}" } = $doc->{$name};
248             }
249             else {
250 0           $scope->{ "$doc->{_id}" } = $doc;
251 0           $doc->{$name} = $class->find_one({ _id=>$_id }, undef, $scope );
252             }
253             }
254             }
255             elsif( $class->isa('Mongoose::Join') ) {
256 0           my $ref_arr = delete( $doc->{$name} );
257 0           my $ref_class = $type->type_parameter->class ;
258             $doc->{$name} = bless {
259             class => $class_main, field => $name, parent => $doc->{_id},
260 0           with_class => $ref_class, children => $ref_arr, buffer => {}
261             } => $class;
262             }
263             }
264             else { #non-moose
265 0           my $data = delete $doc->{$name};
266 0 0         if ( my $data_class = ref $data ) {
267 0 0         $doc->{$name} = $data_class eq 'boolean' ? $data : bless $data => $class;
268             }
269             else {
270 0           push @later, { attrib => $name, value => $data };
271             }
272             }
273             }
274              
275 0 0         return undef unless defined $doc;
276 0           bless $doc => $class_main;
277 0           for ( @later ) {
278 0           my $attr = $class_main->meta->get_attribute($_->{attrib});
279 0 0         if( defined $attr ) {
280             # works for read-only values
281 0           $attr->set_value($doc, $_->{value});
282             } else {
283             # sometimes get_attribute is undef, old method instead:
284 0           my $meth = $_->{attrib};
285 0           $doc->$meth($_->{value});
286             }
287             }
288              
289 0           $doc->expanded;
290 0           $doc;
291             }
292              
293             # Called after doc is expanded, a good point for some black magic
294             # Mostly needed to allow old mongoose document classes to
295             # manipulate dates on nested types.
296       0 1   sub expanded {}
297              
298             sub _joint_fields {
299 0     0     my $self = shift;
300 0           return map { $_->name }
301 0           grep { $_->type_constraint->isa('Mongoose::Join') }
  0            
302             $self->meta->get_all_attributes;
303             }
304              
305             sub fix_integrity {
306 0     0 1   my ($self, @fields ) = @_;
307 0           my $id = $self->_id;
308 0 0         @fields = $self->_joint_fields unless scalar @fields;
309 0           for my $field ( @fields ) {
310 0           my @children = $self->$field->_children_refs;
311 0           $self->collection->update( { _id=>$id }, { '$set'=>{ $field=>\@children } } );
312             }
313             }
314              
315             sub _unbless_full {
316 0     0     require Data::Structure::Util;
317 0           Data::Structure::Util::unbless( shift );
318             }
319              
320 0     0 1   sub save { _save(@_) }
321             sub _save {
322 0     0     my ( $self, @scope ) = @_;
323 0           my $coll = $self->collection;
324 0           my $doc = $self->collapse( @scope );
325 0 0         return unless defined $doc;
326              
327 0 0         if ( my $id = $self->_id ) { ## update on my id
328 0           my $ret = $coll->replace_one( { _id => $id }, $doc, { upsert => 1 } );
329 0           return $id;
330             }
331             else {
332 0 0         if ( ref Mongoose->class_config($self)->{pk} ) {
333             # if we have a pk and no _id, we must have a new
334             # document, so we insert to allow the pk constraint
335             # to ensure uniqueness; the 'safe' parameter ensures
336             # an exception is thrown on a duplicate
337 0           my $id = $coll->insert_one( $doc )->inserted_id;
338 0           $self->_id( $id );
339 0           return $id;
340             }
341             else {
342             # save without pk
343 0           my $id = $coll->insert_one( $doc )->inserted_id;
344 0           $self->_id( $id );
345              
346             # if there are any new, unsaved, documents in the scope,
347             # we have circular relation between $self and @scope
348 0           my @unsaved;
349 0           for my $x ( @scope ) {
350 0 0         unless( $x->_id ) {
351 0           push @unsaved, $x;
352             }
353             }
354              
355 0 0         if (@unsaved) {
356 0           while ( my $x = pop(@unsaved) ) {
357 0           $x->_save(@unsaved);
358             }
359 0           $self->_save;
360             }
361              
362 0           return $id;
363             }
364             }
365             }
366              
367             sub _get_blessed_type {
368 0     0     my ($self,$type) = @_;
369 0 0         my $class = $type->name or return;
370 0           my $parent = $type->parent;
371 0 0         return $class unless defined $parent;
372 0 0         return $class if $parent eq 'Object';
373 0           return $parent->name;
374             }
375              
376             # shallow delete
377             sub delete {
378 0     0 1   my ( $self, $args ) = @_;
379              
380 0 0         if ( ref $args ) {
    0          
381 0           return $self->collection->remove($args);
382             }
383             elsif ( my $pk = $self->_primary_key_query ) {
384 0           return $self->collection->delete_one($pk);
385             }
386              
387 0           return undef;
388             }
389              
390             #sub delete_cascade {
391             # my ($self, $args )=@_;
392             # #TODO delete related collections
393             #}
394              
395             sub db {
396 0     0 1   my $self=shift;
397 0   0       return Mongoose->connection( ref $self || $self )
398             || croak 'MongoDB database need to be configured. Set Mongoose->db(...) first';
399             }
400              
401             sub collection {
402 0     0 1   my $self = shift;
403 0           $self->db->get_collection( $self->_collection_name );
404             }
405              
406             sub _primary_key_query {
407 0     0     my ( $self, $hash ) = @_;
408 0 0         my @keys = @{ Mongoose->class_config($self)->{pk} || ['_id'] };
  0            
409 0           my @pairs = map { $_ => $self->{$_} } grep { $self->{$_} } @keys;
  0            
  0            
410             # Query need to have all pk's
411 0 0         return {@pairs} if @pairs == @keys * 2;
412             }
413              
414 0     0     sub _collection_name { Mongoose->class_config(shift)->{collection_name} }
415              
416             sub find {
417 0     0 1   my $self = shift;
418 0           my $cursor = bless $self->collection->find(@_), 'Mongoose::Cursor';
419 0           $cursor->_collection_name( $self->_collection_name );
420 0   0       $cursor->_class( ref $self || $self );
421 0           return $cursor;
422             }
423              
424             sub query {
425 0     0 1   my $self = shift;
426 0           $self->collection->_warn_deprecated( 'query' => ['find'] );
427 0           $self->find(@_);
428             }
429              
430             sub find_one {
431 0     0 1   my $self = shift;
432              
433 0 0 0       if( @_ == 1 && ( !ref($_[0]) || ref($_[0]) eq 'BSON::OID' ) ) {
      0        
434 0 0 0       my $query = { _id=> ref $_[0] ? $_[0] : eval{BSON::OID->new( oid => pack("H*",$_[0]) )}||$_[0] };
435 0 0         if ( my $doc = $self->collection->find_one($query) ) {
436 0           return $self->expand( $doc );
437             }
438             }
439             else {
440 0           my ($query,$fields, $scope) = @_;
441 0 0         if ( my $doc = $self->collection->find_one( $query, $fields ) ) {
442 0           return $self->expand( $doc, $fields, $scope );
443             }
444             }
445              
446 0           undef;
447             }
448              
449             sub count {
450 0     0 1   my $self = shift;
451 0 0         @_ ? $self->count_documents(@_) : $self->estimated_document_count;
452             }
453 0     0 1   sub count_documents { shift->collection->count_documents(@_) }
454 0     0 1   sub estimated_document_count { shift->collection->estimated_document_count(@_) }
455              
456             =head1 NAME
457              
458             Mongoose::Engine - serialization for MongoDB driver
459              
460             =head1 DESCRIPTION
461              
462             The Mongoose standard engine. Does all the dirty work. Very monolithic.
463             Replace it with your engine if you want.
464              
465             =head1 METHODS
466              
467             =head2 find_one
468              
469             Just like L<MongoDB::Collection/find_one>, but blesses the hash document
470             into your class package.
471              
472             Also has a handy mode which allows
473             retrieving an C<_id> directly from a BSON:OID or just a string:
474              
475             my $author = Author->find_one( '4dd77f4ebf4342d711000000' );
476              
477             Which expands onto:
478              
479             my $author = Author->find_one({
480             _id => BSON::OID->new( value => pack( 'H*', '4dd77f4ebf4342d711000000' ) )
481             });
482              
483             =head2 find
484              
485             Just like L<MongoDB::Collection/find>, but returns
486             a L<Mongoose::Cursor> of documents blessed into
487             your package.
488              
489             =head2 query
490              
491             Just like L<MongoDB::Collection/query>, but returns
492             a L<Mongoose::Cursor> of documents blessed into
493             your package.
494              
495             =head2 count
496              
497             Helper and back-compat method to call estimated_document_count() or count_documents()
498             depending on arguments passed.
499              
500             =head2 estimated_document_count
501              
502             Just like L<MongoDB::Collection/estimated_document_count>.
503              
504             =head2 count_documents
505              
506             Just like L<MongoDB::Collection/count_documents>.
507              
508             =head2 delete
509              
510             Deletes the document in the database.
511              
512             =head2 collapse
513              
514             Turns an object into a hash document.
515              
516             =head2 expand
517              
518             Turns a hash document back into an object.
519              
520             =head2 expanded
521              
522             This is an empty method called by expand() after a document was turned into an object. This
523             was added in version 2 to allow old classes to handle dates on nested types that previously
524             were DateTime objects and now are BSON::Time, but it can be used/abused to any kind of fix
525             to the expanded object.
526              
527             It's recommended to use with care and alway using method modifiers (after) to allow subclassing
528             and composition.
529              
530             =head2 collection
531              
532             Returns the L<MongoDB::Collection> object for this class or object.
533              
534             =head2 save
535              
536             Commits the object to the database.
537              
538             =head2 db
539              
540             Returns the object's corresponding L<MongoDB::Database> instance.
541              
542             =head2 fix_integrity
543              
544             Checks all L<Mongoose::Join> fields for invalid references to
545             foreign object ids.
546              
547             =cut
548              
549             1;