File Coverage

blib/lib/Mandel/Document.pm
Criterion Covered Total %
statement 89 144 61.8
branch 34 62 54.8
condition 7 14 50.0
subroutine 23 35 65.7
pod 12 12 100.0
total 165 267 61.8


line stmt bran cond sub pod time code
1             package Mandel::Document;
2              
3             =head1 NAME
4              
5             Mandel::Document - A single MongoDB document with logic
6              
7             =head1 SYNOPSIS
8              
9             Extend a class with C instead of L:
10              
11             package MyModel::Person;
12             use Mandel::Document "MyDocument::Class";
13              
14             Specify a default collection name, instead of the
15             L. L will think you meant a base
16             class, if this argument contains "::".
17              
18             package MyModel::Person;
19             use Mandel::Document "some_collection_name";
20             use Types::Standard 'Str';
21              
22             field "foo";
23              
24             field "foo" => (
25             isa => Str,
26             builder => sub {
27             my $self = shift;
28             return "default value";
29             },
30             );
31              
32              
33             Spell out the options with a list:
34              
35             package MyModel::Person;
36              
37             use Mandel::Document (
38             extends => "My::Document::Class",
39             collection_name => "some_collection_name",
40             collection_class => "My::Custom::Collection",
41             );
42              
43             =head1 DESCRIPTION
44              
45             L is a simplistic model layer using the L module to interact
46             with a MongoDB backend. The L class defines the overall model,
47             including high level interaction. Individual results, called Types inherit
48             from L.
49              
50             An object of this class gets automatically serialized by L.
51             See L and L for details.
52              
53             Example:
54              
55             use Mojolicious::Lite;
56             # ...
57             get '/some/resource' => sub {
58             my $c = shift;
59             # find some document...
60             $c->render(json => $mandel_doc_object);
61             };
62              
63             =cut
64              
65 15     15   130494 use Mojo::Base 'Mojo::Base';
  15         44182  
  15         102  
66 15     15   13852 use Mojo::JSON::Pointer;
  15         9188  
  15         139  
67 15     15   7205 use Mojo::Util qw( monkey_patch );
  15         389443  
  15         1809  
68 15     15   5022 use Mandel::Model;
  15         46  
  15         146  
69 15     15   6213 use Mango::BSON ':bson';
  15         271220  
  15         3519  
70 15     15   104 use Scalar::Util 'looks_like_number';
  15         33  
  15         691  
71 15     15   80 use Carp 'confess';
  15         30  
  15         1092  
72 15 50   15   83 use constant DEBUG => $ENV{MANDEL_CURSOR_DEBUG} ? eval 'require Data::Dumper;1' : 0;
  15         27  
  15         37911  
73              
74             my $POINTER = Mojo::JSON::Pointer->new;
75              
76             =head1 ATTRIBUTES
77              
78             L inherits all attributes from L and implements the
79             following new ones.
80              
81             =head2 id
82              
83             $object_id = $self->id;
84             $self = $self->id("507f1f77bcf86cd799439011");
85             $self = $self->id(Mango::BSON::ObjectID->new);
86              
87             Returns the L object for this document.
88             Will create one if it does not already exist.
89              
90             This can field can also be set.
91              
92             =cut
93              
94             sub id {
95 11     11 1 1247 my $self = shift;
96 11         30 my $raw = $self->data;
97              
98 11 100       167 if (@_) {
    100          
99 3         10 $self->dirty->{_id} = 1;
100 3 100       20 $raw->{_id} = ref $_[0] ? $_[0] : bson_oid $_[0];
101 3         51 return $self;
102             }
103             elsif ($raw->{_id}) {
104 6         50 return $raw->{_id};
105             }
106             else {
107 2         11 $self->dirty->{_id} = 1;
108 2         14 return $raw->{_id} = bson_oid;
109             }
110             }
111              
112             =head2 data
113              
114             $hash = $self->data;
115             $self = $self->data($hash);
116              
117             Holds the raw mongodb document. It is possible to define default values for
118             this attribute by defining L for the
119             fields.
120              
121             =head2 in_storage
122              
123             Boolean true if this document has been fetched from storage or L
124             to storage.
125              
126             =head2 connection
127              
128             An instance of L. This is required.
129              
130             =head2 model
131              
132             Returns a L object. This object is a class variable and
133             therefor shared between all instances.
134              
135             =head2 dirty
136              
137             This attribute holds a hash-ref where the keys are name of fields that has
138             been updated or otherwise not stored in database.
139              
140             TODO: Define what the values should hold. Timestamp? A counter for how
141             many times the field has been updated before saved..?
142              
143             =cut
144              
145             has connection => sub { confess "connection required in constructor" };
146             has model => sub { confess "model required in constructor" };
147             has dirty => sub { +{} };
148             has in_storage => 0;
149              
150             has _storage_collection => sub {
151             my $self = shift;
152             $self->connection->_storage_collection($self->model->collection_name);
153             };
154              
155             has data => sub { shift->_build_data }; # raw mongodb document data
156              
157 8     8   118 sub _build_data { +{} }
158              
159             =head1 METHODS
160              
161             L inherits all of the methods from L and
162             implements the following new ones.
163              
164             =head2 new
165              
166             Constructs a new object.
167              
168             =cut
169              
170             sub new {
171 13     13 1 4687 my $self = shift->SUPER::new(@_);
172 13 100       210 $self->id(delete $self->{id}) if $self->{id};
173 13         63 $self;
174             }
175              
176             =head2 initialize
177              
178             A no-op placeholder useful for initialization. See L.
179              
180             =cut
181              
182 1     1 1 927 sub initialize {shift}
183              
184             =head2 contains
185              
186             $bool = $self->contains('/json/2/pointer');
187              
188             Use L to check if a value exists inside the raw
189             mongodb document.
190              
191             =cut
192              
193             sub contains {
194 3     3 1 977 my $self = shift;
195 3         9 $POINTER->data($self->data)->contains(@_);
196             }
197              
198             =head2 fresh
199              
200             $self = $self->fresh;
201              
202             Calling this method will force the next relationship call to return fresh
203             data from database instead of cached. Example:
204              
205             $self->fresh->cats(sub {
206             my($self, $err, $cats) = @_;
207             });
208              
209             =cut
210              
211             sub fresh {
212 0     0 1 0 $_[0]->{fresh} = 1;
213 0         0 $_[0];
214             }
215              
216             =head2 get
217              
218             $any = $self->get('/json/2/pointer');
219              
220             Use L to retrieve a value inside the raw mongodb
221             document.
222              
223             =cut
224              
225             sub get {
226 2     2 1 791 my $self = shift;
227 2         7 $POINTER->data($self->data)->get(@_);
228             }
229              
230             =head2 is_changed
231              
232             Returns true if L contains any field names.
233              
234             =cut
235              
236             sub is_changed {
237 2 100   2 1 17 return 0 unless $_[0]->{dirty};
238 1 50       3 return 0 unless keys %{$_[0]->{dirty}};
  1         6  
239 1         10 return 1;
240             }
241              
242             =head2 patch
243              
244             $self = $self->patch(\%changes, sub { my($self, $err) = @_ });
245             $self = $self->patch(\%changes);
246              
247             This method will insert/update a partial document. This is useful if C
248             does not contain a complete document.
249              
250             It will also insert the document if a document with L does not already
251             exist.
252              
253             =cut
254              
255             sub patch {
256 0     0 1 0 my ($self, $changes, $cb) = @_;
257 0         0 my $data = $self->data;
258              
259 0 0       0 if ($changes) {
260 0         0 @$data{keys %$changes} = values %$changes;
261             }
262              
263 0         0 $data = {%$data};
264 0         0 delete $data->{_id}; # Mod on _id not allowed
265              
266             $self->_storage_collection->update(
267             {_id => $self->id},
268             {'$set' => $data},
269             {upsert => bson_true},
270             $cb
271             ? (
272             sub {
273 0 0   0   0 $self->_mark_stored_clean unless $_[1];
274 0         0 $self->$cb($_[1]);
275             }
276             )
277 0 0       0 : (),
278             );
279              
280 0 0       0 $self->_mark_stored_clean unless $cb;
281 0         0 $self;
282             }
283              
284             =head2 remove
285              
286             $self = $self->remove(sub { my($self, $err) = @_; });
287             $self = $self->remove;
288              
289             Will remove this object from the L and set mark
290             all fields as L.
291              
292             =cut
293              
294             sub remove {
295 0     0 1 0 my ($self, $cb) = @_;
296 0         0 my $c = $self->_storage_collection;
297 0         0 my @args = ({_id => $self->id}, {single => 1});
298              
299 0         0 warn "[$self\::remove] @{[$self->id]}\n" if DEBUG;
300              
301 0 0       0 if ($cb) {
302             $c->remove(
303             @args,
304             sub {
305 0     0   0 my ($collection, $err, $doc) = @_;
306 0 0       0 $self->_mark_removed_dirty unless $err;
307 0         0 $self->$cb($err);
308             }
309 0         0 );
310             }
311             else {
312 0         0 $c->remove(@args);
313 0         0 $self->_mark_removed_dirty;
314             }
315              
316 0         0 return $self;
317             }
318              
319             sub _mark_removed_dirty {
320 0     0   0 my $self = shift;
321 0         0 $self->dirty->{$_} = 1 for keys %{$self->data};
  0         0  
322 0         0 $self->in_storage(0);
323             }
324              
325             =head2 save
326              
327             $self = $self->save(sub { my($self, $err) = @_; });
328             $self = $self->save;
329              
330             This method stores the raw data in the database and collection. It clear
331             the L attribute.
332              
333             NOTE: This method will call the callback (with $err set to empty string)
334             immediately unless L is true and L is false.
335              
336             =cut
337              
338             sub save {
339 0     0 1 0 my ($self, $cb) = @_;
340              
341 0 0 0     0 if (!$self->is_changed and $self->in_storage) {
342 0 0       0 $self->$cb('') if $cb;
343 0         0 return $self;
344             }
345              
346 0         0 $self->id; # make sure we have an ObjectID
347              
348 0         0 warn "[$self\::save] ", Data::Dumper->new([$self->data])->Indent(1)->Sortkeys(1)->Terse(1)->Dump if DEBUG;
349 0         0 my $c = $self->_storage_collection;
350              
351 0 0       0 if ($cb) {
352             $c->save(
353             $self->data,
354             sub {
355 0     0   0 my ($collection, $err, $doc) = @_;
356 0 0       0 $self->_mark_stored_clean unless $err;
357 0         0 $self->$cb($err);
358             }
359 0         0 );
360             }
361             else {
362 0         0 $c->save($self->data);
363 0         0 $self->_mark_stored_clean;
364             }
365              
366 0         0 return $self;
367             }
368              
369             sub _mark_stored_clean {
370 0     0   0 my $self = shift;
371 0         0 delete $self->{dirty};
372 0         0 $self->in_storage(1);
373             }
374              
375             =head2 set
376              
377             $self = $self->set('/json/2/pointer', $val);
378              
379             Use a JSON pointer to set data in the raw mongodb document. This method will
380             die if the pointer points to non-compatible data.
381              
382             =cut
383              
384             sub set {
385 8     8 1 2638 my ($self, $pointer, $val) = @_;
386 8         26 my $raw = $self->data;
387 8         40 my (@path, $field);
388              
389 8 50       66 return $self unless $pointer =~ s!^/!!;
390 8         27 @path = split '/', $pointer;
391 8         12 $field = $path[0];
392              
393 8         18 while (@path) {
394 26         41 my $p = shift @path;
395 26         41 my $type = ref $raw;
396 26 100       76 my $want = looks_like_number $p ? 'INDEX' : 'KEY';
397              
398 26 100       57 if ($type eq 'HASH') {
    100          
399 16 100       28 if (@path) {
400 13 50 66     62 $raw = $raw->{$p} ||= looks_like_number $path[0] ? [] : {};
401             }
402             else {
403 3         12 $raw->{$p} = $val;
404             }
405             }
406             elsif ($type eq 'ARRAY') {
407 8 100       23 if ($want ne 'INDEX') {
    100          
408 1         197 confess "Cannot set $want in $type for /$pointer ($p)";
409             }
410             elsif (@path) {
411 5 50 66     34 $raw = $raw->[$p] ||= looks_like_number $path[0] ? [] : {};
412             }
413             else {
414 2         9 $raw->[$p] = $val;
415             }
416             }
417             else {
418 2         262 confess "Cannot set $want in SCALAR for /$pointer ($p)";
419             }
420             }
421              
422 5 50       23 $self->dirty->{$field} = 1 if defined $field;
423 5         40 $self;
424             }
425              
426             =head2 import
427              
428             See L.
429              
430             =cut
431              
432             sub import {
433 13     13   453 my $class = shift;
434 13 100       55 my %args = @_ == 1 ? (name => shift) : @_;
435 13         32 my $caller = caller;
436 13         107 my $model = Mandel::Model->new(document_class => $caller, %args);
437 13         117 my $base_class = 'Mandel::Document';
438              
439 13         32 for (qw/name extends/) {
440 26 100 100     131 if ($args{$_} and $args{$_} =~ /::/) {
441 1         3 $base_class = delete $args{$_};
442             }
443             }
444 13 100       55 if (!$args{name}) {
445 11         85 $args{name} = Mojo::Util::decamelize(($caller =~ /(\w+)$/)[0]);
446 11         212 $model->name($args{name});
447             }
448              
449 13     0   173 monkey_patch $caller, belongs_to => sub { $model->relationship(belongs_to => @_)->monkey_patch };
  0     0   0  
450 13     12   290 monkey_patch $caller, field => sub { $model->field(shift, {@_}) };
  12         3916  
451 13     12   312 monkey_patch $caller, has_many => sub { $model->relationship(has_many => @_)->monkey_patch };
  0         0  
452 13     1   247 monkey_patch $caller, has_one => sub { $model->relationship(has_one => @_)->monkey_patch };
  0         0  
453 13     0   264 monkey_patch $caller, list_of => sub { $model->relationship(list_of => @_)->monkey_patch };
  0         0  
454 13     12   248 monkey_patch $caller, model => sub {$model};
  12         7047  
455              
456 13         219 @_ = ($class, $base_class);
457 13         61 goto &Mojo::Base::import;
458             }
459              
460             =head2 TO_JSON
461              
462             Alias for L.
463              
464             This method allow the document to get automatically serialized by
465             L.
466              
467             =cut
468              
469 1     13 1 491 sub TO_JSON { shift->data }
470              
471             sub _cache {
472 0     5     my $self = shift;
473 0   0       my $cache = $self->{cache} ||= {};
474              
475 0 0         return $cache->{$_[0]} if @_ == 1; # get
476 0           return $cache->{$_[0]} = $_[1]; # set
477             }
478              
479             =head1 SEE ALSO
480              
481             L, L, L
482              
483             =head1 AUTHOR
484              
485             Jan Henning Thorsen - C
486              
487             =cut
488              
489             1;