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