File Coverage

blib/lib/Mongoose/Engine.pm
Criterion Covered Total %
statement 18 233 7.7
branch 0 156 0.0
condition 0 41 0.0
subroutine 6 26 23.0
pod 11 11 100.0
total 35 467 7.4


line stmt bran cond sub pod time code
1             package Mongoose::Engine;
2             $Mongoose::Engine::VERSION = '1.02';
3 1     1   545 use Moose::Role;
  1         1  
  1         7  
4              
5 1     1   3378 use Carp;
  1         2  
  1         66  
6 1     1   3 use Scalar::Util qw/refaddr reftype/;
  1         2  
  1         37  
7 1     1   4 use List::Util qw/first/;
  1         1  
  1         51  
8 1     1   5 use boolean;
  1         1  
  1         9  
9              
10 1     1   372 use Mongoose::Cursor; #initializes moose
  1         286  
  1         2215  
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 MongoDB::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} = MongoDB::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 MongoDB::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, MongoDB::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 = MongoDB::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 )$/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 {
147 0     0 1   my ( $self, $doc, $fields, $scope ) = @_;
148 0           my $config = Mongoose->class_config($self);
149 0   0       my $class_main = ref $self || $self;
150 0           my @later;
151              
152 0 0         $scope = {} unless ref $scope eq 'HASH';
153              
154             # check if it's an straight ref
155 0 0         if ( ref $doc eq 'MongoDB::DBRef' ) {
156             return defined $scope->{$doc->id}
157 0 0         ? $scope->{$doc->id}
158             : $class_main->find_one({ _id => $doc->id });
159             #TODO: set at $scope?
160             }
161              
162 0           for my $attr ( $class_main->meta->get_all_attributes ) {
163 0           my $name = $attr->name;
164              
165 0 0         next unless exists $doc->{$name};
166 0 0         next if $attr->does('Mongoose::Meta::Attribute::Trait::Raw');
167              
168 0 0         my $type = $attr->type_constraint or next;
169 0 0         my $class = $self->_get_blessed_type( $type ) or next;
170              
171 0 0         if ( $type->is_a_type_of('HashRef') ) {
    0          
    0          
172             # HashRef[ parameter ]
173 0 0         if( defined $type->{type_parameter} ) {
174 0           my $param = $type->{type_parameter};
175 0 0         if( my $param_class = $param->{class} ) {
176 0 0         for my $key ( keys %{ $doc->{$name} || {} } ) {
  0            
177 0           $doc->{$name}{$key} = $param_class->expand( $doc->{$name}{$key}, undef, $scope );
178             }
179             }
180             }
181              
182 0           next;
183             }
184             elsif ( $type->is_a_type_of('ArrayRef') ) {
185 0 0         if ( defined $type->{type_parameter} ) {
186             # ArrayRef[ parameter ]
187 0           my $param = $type->{type_parameter};
188 0 0         if( my $param_class = $param->{class} ) {
189 0           my @objs;
190 0 0         for my $item ( @{ $doc->{$name} || [] } ) {
  0            
191             #TODO: why not to call myself always like:
192             #push @obj, $param_class->expand( $item, undef, $scope );
193 0 0         if ( $param_class->does('Mongoose::EmbeddedDocument') ) {
    0          
194 0           push @objs, $param_class->expand($item);
195             }
196             elsif ( $param_class->does('Mongoose::Document') ) {
197 0 0         if( my $circ_doc = $scope->{ $item->id } ) {
198 0           push @objs, bless( $circ_doc , $param_class );
199             }
200             else {
201 0 0         if ( my $obj = $param_class->find_one({ _id => $item->id }, undef, $scope ) ) {
202 0           push @objs, $obj;
203             }
204             }
205             }
206             }
207 0           $doc->{$name} = \@objs;
208             }
209             else {
210 0   0       $doc->{$name} ||= [];
211             }
212             }
213              
214 0           next;
215             }
216             elsif( $type->is_a_type_of('FileHandle') ) {
217             $doc->{$name} = Mongoose::File->new(
218 0           file_id => $doc->{$name}->id,
219             bucket => $self->db->gfs
220             );
221 0           next;
222             }
223              
224 0 0         if( $class->can('meta') ) { # moose subobject
225              
226 0 0         if ( $class->does('Mongoose::EmbeddedDocument') ) {
    0          
    0          
227 0           $doc->{$name} = bless $doc->{$name}, $class;
228             }
229             elsif ( $class->does('Mongoose::Document') ) {
230 0 0         if ( ref $doc->{$name} eq 'MongoDB::DBRef' ) {
231 0           my $_id = $doc->{$name}->id;
232 0 0         if ( my $circ_doc = $scope->{"$_id"} ) {
233 0           $doc->{$name} = bless( $circ_doc , $class );
234 0           $scope->{ "$circ_doc->{_id}" } = $doc->{$name};
235             }
236             else {
237 0           $scope->{ "$doc->{_id}" } = $doc;
238 0           $doc->{$name} = $class->find_one({ _id=>$_id }, undef, $scope );
239             }
240             }
241             }
242             elsif( $class->isa('Mongoose::Join') ) {
243 0           my $ref_arr = delete( $doc->{$name} );
244 0           my $ref_class = $type->type_parameter->class ;
245             $doc->{$name} = bless {
246             class => $class_main, field => $name, parent => $doc->{_id},
247 0           with_class => $ref_class, children => $ref_arr, buffer => {}
248             } => $class;
249             }
250             }
251             else { #non-moose
252 0           my $data = delete $doc->{$name};
253 0 0         if ( my $data_class = ref $data ) {
254 0 0         $doc->{$name} = $data_class eq 'boolean' ? $data : bless $data => $class;
255             }
256             else {
257 0           push @later, { attrib => $name, value => $data };
258             }
259             }
260             }
261              
262 0 0         return undef unless defined $doc;
263 0           bless $doc => $class_main;
264 0           for ( @later ) {
265 0           my $attr = $class_main->meta->get_attribute($_->{attrib});
266 0 0         if( defined $attr ) {
267             # works for read-only values
268 0           $attr->set_value($doc, $_->{value});
269             } else {
270             # sometimes get_attribute is undef, old method instead:
271 0           my $meth = $_->{attrib};
272 0           $doc->$meth($_->{value});
273             }
274             }
275              
276 0           $doc;
277             }
278              
279             sub _joint_fields {
280 0     0     my $self = shift;
281 0           return map { $_->name }
282 0           grep { $_->type_constraint->isa('Mongoose::Join') }
  0            
283             $self->meta->get_all_attributes;
284             }
285              
286             sub fix_integrity {
287 0     0 1   my ($self, @fields ) = @_;
288 0           my $id = $self->_id;
289 0 0         @fields = $self->_joint_fields unless scalar @fields;
290 0           for my $field ( @fields ) {
291 0           my @children = $self->$field->_children_refs;
292 0           $self->collection->update( { _id=>$id }, { '$set'=>{ $field=>\@children } } );
293             }
294             }
295              
296             sub _unbless_full {
297 0     0     require Data::Structure::Util;
298 0           Data::Structure::Util::unbless( shift );
299             }
300              
301 0     0 1   sub save { _save(@_) }
302             sub _save {
303 0     0     my ( $self, @scope ) = @_;
304 0           my $coll = $self->collection;
305 0           my $doc = $self->collapse( @scope );
306 0 0         return unless defined $doc;
307              
308 0 0         if ( my $id = $self->_id ) { ## update on my id
309 0           my $ret = $coll->replace_one( { _id => $id }, $doc, { upsert => 1 } );
310 0           return $id;
311             }
312             else {
313 0 0         if ( ref Mongoose->class_config($self)->{pk} ) {
314             # if we have a pk and no _id, we must have a new
315             # document, so we insert to allow the pk constraint
316             # to ensure uniqueness; the 'safe' parameter ensures
317             # an exception is thrown on a duplicate
318 0           my $id = $coll->insert_one( $doc )->inserted_id;
319 0           $self->_id( $id );
320 0           return $id;
321             }
322             else {
323             # save without pk
324 0           my $id = $coll->insert_one( $doc )->inserted_id;
325 0           $self->_id( $id );
326              
327             # if there are any new, unsaved, documents in the scope,
328             # we have circular relation between $self and @scope
329 0           my @unsaved;
330 0           for my $x ( @scope ) {
331 0 0         unless( $x->_id ) {
332 0           push @unsaved, $x;
333             }
334             }
335              
336 0 0         if (@unsaved) {
337 0           while ( my $x = pop(@unsaved) ) {
338 0           $x->_save(@unsaved);
339             }
340 0           $self->_save;
341             }
342              
343 0           return $id;
344             }
345             }
346             }
347              
348             sub _get_blessed_type {
349 0     0     my ($self,$type) = @_;
350 0 0         my $class = $type->name or return;
351 0           my $parent = $type->parent;
352 0 0         return $class unless defined $parent;
353 0 0         return $class if $parent eq 'Object';
354 0           return $parent->name;
355             }
356              
357             # shallow delete
358             sub delete {
359 0     0 1   my ( $self, $args ) = @_;
360              
361 0 0         if ( ref $args ) {
    0          
362 0           return $self->collection->remove($args);
363             }
364             elsif ( my $pk = $self->_primary_key_query ) {
365 0           return $self->collection->delete_one($pk);
366             }
367              
368 0           return undef;
369             }
370              
371             #sub delete_cascade {
372             # my ($self, $args )=@_;
373             # #TODO delete related collections
374             #}
375              
376             sub db {
377 0     0 1   my $self=shift;
378 0   0       return Mongoose->_db_for_class( ref $self || $self )
379             || croak 'MongoDB not set. Set Mongoose->db("name") first';
380             }
381              
382             sub collection {
383 0     0 1   my ($self, $new_collection) = @_;
384 0           my $db = $self->db;
385              
386             # getter
387 0           my $config = Mongoose->class_config($self);
388             $new_collection or return $config->{collection}
389 0 0 0       || ( $config->{collection} = $db->get_collection( $config->{collection_name} ) );
390              
391             # setter
392 0           my $is_singleton = ! ref $self;
393 0 0         if( ref($new_collection) eq 'MongoDB::Collection' ) {
    0          
394             # changing collection objects directly
395 0 0         if( $is_singleton ) {
396 0           $config->{collection_name} = $new_collection->name;
397 0           return $config->{collection} = $new_collection;
398             } else {
399 0           my $class = ref $self;
400 0           Carp::confess "Changing the object collection is not currently supported. Use $class->collection() instead";
401             }
402             }
403             elsif( $new_collection ) {
404             # setup a new collection by name
405 0 0         if( $is_singleton ) {
406 0           $config->{collection_name} = $new_collection;
407 0           return $config->{collection} = $db->get_collection( $new_collection );
408             } else {
409 0           my $class = ref $self;
410 0           Carp::confess "Changing the object collection is not currently supported. Use $class->collection() instead";
411             }
412             }
413             }
414              
415             sub _primary_key_query {
416 0     0     my ( $self, $hash ) = @_;
417 0 0         my @keys = @{ Mongoose->class_config($self)->{pk} || ['_id'] };
  0            
418 0           my @pairs = map { $_ => $self->{$_} } grep { $self->{$_} } @keys;
  0            
  0            
419             # Query need to have all pk's
420 0 0         return {@pairs} if @pairs == @keys * 2;
421             }
422              
423 0     0     sub _collection_name { Mongoose->class_config(shift)->{collection_name} }
424              
425             sub find {
426 0     0 1   my $self = shift;
427 0           my $cursor = bless $self->collection->find(@_), 'Mongoose::Cursor';
428 0           $cursor->_collection_name( $self->_collection_name );
429 0   0       $cursor->_class( ref $self || $self );
430 0           return $cursor;
431             }
432              
433             sub query {
434 0     0 1   my $self = shift;
435 0           $self->collection->_warn_deprecated( 'query' => ['find'] );
436 0           $self->find(@_);
437             }
438              
439             sub find_one {
440 0     0 1   my $self = shift;
441              
442 0 0 0       if( @_ == 1 && ( !ref($_[0]) || ref($_[0]) eq 'MongoDB::OID' ) ) {
      0        
443 0 0 0       my $query = { _id=> ref $_[0] ? $_[0] : eval{MongoDB::OID->new( value=>$_[0] )}||$_[0] };
444 0 0         if ( my $doc = $self->collection->find_one($query) ) {
445 0           return $self->expand( $doc );
446             }
447             }
448             else {
449 0           my ($query,$fields, $scope) = @_;
450 0 0         if ( my $doc = $self->collection->find_one( $query, $fields ) ) {
451 0           return $self->expand( $doc, $fields, $scope );
452             }
453             }
454              
455 0           undef;
456             }
457              
458 0     0 1   sub count { shift->collection->count(@_) }
459              
460             =head1 NAME
461              
462             Mongoose::Engine::V1 - serialization for MongoDBv1 driver
463              
464             =head1 DESCRIPTION
465              
466             The Mongoose standard engine. Does all the dirty work. Very monolithic.
467             Replace it with your engine if you want.
468              
469             =head1 METHODS
470              
471             =head2 find_one
472              
473             Just like L<MongoDB::Collection/find_one>, but blesses the hash document
474             into your class package.
475              
476             Also has a handy mode which allows
477             retrieving an C<_id> directly from a MongoDB::OID or just a string:
478              
479             my $author = Author->find_one( '4dd77f4ebf4342d711000000' );
480              
481             Which expands onto:
482              
483             my $author = Author->find_one({
484             _id=>MongoDB::OID->new( value=>'4dd77f4ebf4342d711000000' )
485             });
486              
487             =head2 find
488              
489             Just like L<MongoDB::Collection/find>, but returns
490             a L<Mongoose::Cursor> of documents blessed into
491             your package.
492              
493             =head2 query
494              
495             Just like L<MongoDB::Collection/query>, but returns
496             a L<Mongoose::Cursor> of documents blessed into
497             your package.
498              
499             =head2 count
500              
501             Just like L<MongoDB::Collection/count>.
502              
503             =head2 delete
504              
505             Deletes the document in the database.
506              
507             =head2 collapse
508              
509             Turns an object into a hash document.
510              
511             =head2 expand
512              
513             Turns a hash document back into an object.
514              
515             =head2 collection
516              
517             Returns the L<MongoDB::Collection> object for this class or object.
518              
519             =head2 save
520              
521             Commits the object to the database.
522              
523             =head2 db
524              
525             Returns the object's corresponding L<MongoDB::Database> instance.
526              
527             =head2 fix_integrity
528              
529             Checks all L<Mongoose::Join> fields for invalid references to
530             foreign object ids.
531              
532             =cut
533              
534             1;