File Coverage

blib/lib/MongoDBx/Tiny/Document.pm
Criterion Covered Total %
statement 72 276 26.0
branch 1 78 1.2
condition 1 23 4.3
subroutine 22 56 39.2
pod 14 21 66.6
total 110 454 24.2


line stmt bran cond sub pod time code
1             package MongoDBx::Tiny::Document;
2              
3 1     1   2415 use 5.006;
  1         4  
  1         38  
4 1     1   6 use strict;
  1         2  
  1         181  
5 1     1   7 use warnings;
  1         3  
  1         42  
6              
7             =head1 NAME
8              
9             MongoDBx::Tiny::Document - document class
10              
11             =head1 SYNOPSIS
12              
13             package My::Data::Foo;
14             use strict;
15             use MongoDBx::Tiny::Document;
16              
17             COLLECTION_NAME 'foo';
18              
19             # FIELD NAME, sub{}, sub{}..
20             ESSENTIAL q/code/; # like CDBI's Essential.
21             FIELD 'code', INT, LENGTH(10), DEFAULT('0'), REQUIRED;
22             FIELD 'name', STR, LENGTH(30), DEFAULT('noname');
23              
24             # RELATION ACCESSOR, sub{}
25             RELATION 'bar', RELATION_DEFAULT('single','foo_id','id');
26              
27             INDEX 'code',{ unique => 1 };
28             INDEX 'name';
29             INDEX [code => 1, name => -1];
30              
31             sub process_some {
32             my ($class,$tiny,$validator) = @_;
33             $tiny->insert($class->collection_name,$validator->document);
34             }
35              
36              
37             package My::Data::Bar;
38             use strict;
39             use MongoDBx::Tiny::Document;
40              
41             COLLECTION_NAME 'bar';
42             ESSENTIAL qw/foo_id code/;
43             FIELD 'foo_id', OID, DEFAULT(''), REQUIRED;
44             FIELD 'code', INT(10), DEFAULT('0'),REQUIRED;
45             FIELD 'name', VARCHAR(30), DEFAULT('noname'),&MY_ATTRIBUTE;
46              
47             RELATION 'foo', RELATION_DEFAULT('single','id','foo_id');
48              
49             TRIGGER 'before_insert', sub {
50             my ($document_class,$tiny,$document,$opt) = @_;
51             };
52              
53             # before_update,after_update,before_remove,after_remove
54             TRIGGER 'after_insert', sub {
55             my ($document_class,$object,$opt) = @_;
56             };
57              
58             QUERY_ATTRIBUTES {
59             # no support in update and delete
60             single => { del_flag => "off" },
61             search => { del_flag => "off" }
62             };
63              
64             sub MY_ATTRIBUTE {
65             return {
66             name => 'MY_ATTRIBUTE',
67             callback => sub {
68             return 1;
69             }
70             };
71             }
72              
73             =cut
74              
75 1     1   7 use Data::Dumper;
  1         3  
  1         60  
76 1     1   6 use Scalar::Util qw(blessed);
  1         2  
  1         110  
77 1     1   1183 use Class::Trigger;
  1         1633  
  1         10  
78 1     1   65 use Carp qw/carp confess/;
  1         3  
  1         67  
79 1     1   8 use MongoDBx::Tiny::Util;
  1         2  
  1         91  
80 1     1   7 use Params::Validate;
  1         2  
  1         78  
81              
82             use overload
83 1         14 '""' => \&id,
84 1     1   7 'fallback' => 1;
  1         3  
85              
86             sub import {
87 1   50 1   18 my $class = shift || __PACKAGE__;
88 1         9 my $caller = (caller(0))[0];
89             {
90 1     1   138 no strict 'refs';
  1         2  
  1         403  
  1         3  
91 1         2 push @{"${caller}::ISA"}, $class;
  1         18  
92             }
93 1         14 strict->import;
94 1         14 warnings->import;
95 1         277 __PACKAGE__->export_to_level(1, @_);
96 1 50       16 if (__PACKAGE__ ne $class) {
97 0           $class->export_to_level(1,@_);
98             }
99             }
100              
101             =head1 EXPORT
102              
103             A list of functions that can be exported.
104              
105             =head2 COLLECTION_NAME
106              
107             # define collection name.
108             COLLECTION_NAME 'collection_name';
109              
110             =head2 ESSENTIAL
111              
112             # define essential field always fetched.
113             ESSENTIAL qw/field1 field2 field3/;
114              
115             =head2 FIELD
116              
117             # define field name and validation.
118             FIELD 'field_name', CODE, CODE;
119              
120             =head2 RELATION
121              
122             RELATION 'relation_name', RELATION_NAME;
123              
124             sub RELATION_NAME {
125             my $self = shift;
126             my $c_name = shift; # relation
127             my $tiny = $self->tiny;
128             # xxx
129             }
130              
131             =head2 TRIGGER
132              
133             [EXPERIMENTAL]
134              
135             TRIGGER 'phase', CODE;
136              
137             =head2 QUERY_ATTRIBUTES
138            
139             [EXPERIMENTAL]
140              
141             QUERY_ATTRIBUTES {
142             # no support in update and delete
143             single => { del_flag => "off" },
144             search => { del_flag => "off" }
145             };
146              
147             TODO: no_query option for condition
148              
149             =head2 INDEX
150            
151             [EXPERIMENTAL]
152              
153             INDEX 'field_1';
154             INDEX 'field_2',{ unique => 1,drop_dups => 1, safe => 1, background => 1, name => 'foo' };
155             INDEX [field_2 => 1, field_3 => -1];
156              
157             # for manage index
158             $tiny->set_indexes('collection_name');
159              
160             =head2 MongoDBx::Tiny::Attributes::EXPORT
161              
162             perldoc MongoDBx::Tiny::Attributes
163              
164             =head2 MongoDBx::Tiny::Relation::EXPORT
165              
166             perldoc MongoDBx::Tiny::Relation
167              
168             =cut
169              
170              
171             require Exporter;
172             our @ISA = qw/Exporter/;
173             our @EXPORT = qw/COLLECTION_NAME ESSENTIAL FIELD RELATION TRIGGER QUERY_ATTRIBUTES INDEX/;
174 1     1   717 use MongoDBx::Tiny::Attributes;
  1         2  
  1         174  
175 1     1   641 use MongoDBx::Tiny::Relation;
  1         4  
  1         124  
176             push @EXPORT,@{MongoDBx::Tiny::Attributes::EXPORT};
177             push @EXPORT,@{MongoDBx::Tiny::Relation::EXPORT};
178              
179             our $_COLLECTION_NAME;
180             our $_ESSENTIAL;
181             our $_FIELD;
182             our $_RELATION;
183              
184             {
185 1     1   7 no warnings qw(once);
  1         2  
  1         640  
186             *COLLECTION_NAME = \&install_collection_name;
187             *ESSENTIAL = \&install_essential;
188             *FIELD = \&install_field;
189             *RELATION = \&install_relation;
190             *TRIGGER = \&install_trigger;
191             *QUERY_ATTRIBUTES = \&install_query_attributes;
192             *INDEX = \&install_index;
193             }
194              
195 0     0 0   sub install_collection_name { util_class_attr('COLLECTION_NAME',@_) }
196              
197 0     0 0   sub install_essential{ util_class_attr('ESSENTIAL',@_) }
198              
199             sub install_field {
200 0     0 0   my ($proto) = shift;
201 0           my ($class,$stat) = util_guess_class($proto);
202 0           my $name;
203 0 0         if ($stat->{caller}) {
204 0           $name = $proto;
205             }
206              
207              
208 0           my $attr = 'FIELD';
209              
210 0   0       my $field_obj = util_class_attr($attr,$class) ||
211             MongoDBx::Tiny::Document::Field->new;
212              
213 0 0         Carp::croak q/FIELD needs attributes/ unless @_;
214              
215 0 0         if (@_) {
216 0           my (@type) = @_;
217 0           $field_obj->add($name,\@type);
218 0           util_class_attr($attr,$class,$field_obj);
219              
220 0 0         unless ($class->can($name)) {
221             my $accessor = sub {
222 0     0     my $self = shift;
223 0 0         unless ($self->_completed){
224 0           my $essential = $self->essential;
225 0 0         if (!$essential->{$name}) {
226 0           my @not_complete = grep { !$essential->{$_}} $self->field->list;
  0            
227 0           my $doc = $self->collection->find_one(
228 0           {_id => $self->id},{ map { $_ => 1 } @not_complete }
229             );
230 0           for (@not_complete) {
231 0           $self->{$_} = $doc->{$_};
232             }
233 0           $self->_completed(1);
234             }
235             }
236              
237 0 0         if(@_ >= 1) {
238 0           $self->_changed($name);
239             }
240 0 0         if(@_ == 1) {
    0          
241 0           return $self->{$name} = $_[0];
242             } elsif(@_ > 1) {
243 0           return $self->{$name} = [@_];
244             } else {
245 0           return $self->{$name};
246             }
247 0           };
248             {
249 1     1   8 no strict 'refs';
  1         2  
  1         211  
  0            
250 0           *{"${class}::${name}"} = $accessor;
  0            
251             }
252             }
253             }
254 0           return $field_obj;
255             }
256              
257             sub install_relation {
258 0     0 0   my $proto = shift;
259              
260 0           my ($class,$stat) = util_guess_class($proto);
261              
262 0           my $c_name;
263 0 0         if ($stat->{caller}) {
264 0           $c_name = $proto;
265             } else {
266 0           $c_name = shift;
267             }
268              
269 0           my $attr = 'RELATION';
270              
271 0   0       my $relation = util_class_attr($attr,$class) ||
272             MongoDBx::Tiny::Document::Relation->new;
273 0 0         if (@_) {
274 0           my ($clause) = @_;
275 0           $relation->add($c_name => [$clause]);
276 0           util_class_attr($attr,$class,$relation);
277              
278 0 0         unless ($class->can($c_name)) {
279             {
280 1     1   6 no strict 'refs';
  1         2  
  1         1064  
  0            
281 0           *{$class . "::" . $c_name} = sub {
282 0     0     my $self = shift;
283 0           $clause->($self,$c_name);
284             }
285 0           }
286             }
287             }
288 0           return $relation;
289             }
290              
291             sub install_trigger {
292 0     0 0   my ($proto) = shift;
293 0           my ($class,$stat) = util_guess_class($proto);
294 0           my $name;
295 0 0         if ($stat->{caller}) {
296 0           $name = $proto;
297             }
298 0 0         if(@_) {
299 0           my $trigger = util_class_attr('TRIGGER',$class);
300 0           $trigger->{$name} ++;
301 0           util_class_attr('TRIGGER',$class,$trigger);
302             }
303 0           return $class->add_trigger($name,@_);
304             }
305              
306 0     0 0   sub install_query_attributes{ util_class_attr('QUERY_ATTRIBUTES',@_) }
307              
308             sub install_index {
309 0     0 0   my ($proto) = shift;
310 0           my ($class,$stat) = util_guess_class($proto);
311 0           my $name;
312 0 0         if ($stat->{caller}) {
313 0           $name = $proto;
314             }
315              
316 0           my $tmp;
317 0 0         if ($name) {
318 0           my ($index_opt,$opt) = @_;
319 0   0       $tmp = util_class_attr('INDEXES') || [];
320 0           push @$tmp,[ $name,$index_opt,$opt];
321             }
322              
323 0           util_class_attr('INDEXES',$tmp);
324              
325             }
326              
327             =head1 SUBROUTINES/METHODS
328              
329             =head2 new
330              
331             $document_object = $document_class->new($document,$tiny);
332              
333             =cut
334              
335             sub new {
336 0     0 1   my $class = shift;
337 0 0         my $document = shift or confess q/no document/;
338 0 0         my $tiny = shift or confess q/no tiny/;
339 0           my $self = bless $document , $class;
340 0           $self->{_tiny} = $tiny;
341 0           $self->{_changed} = {}; # field is changed or not
342 0           $self->{_completed} = 0; # all fields are fetched or not.
343 0           return $self;
344             }
345              
346             sub _changed {
347 0     0     my $self = shift;
348 0           my $field = shift;
349 0 0         $self->{_changed}->{$field} = 1 if $field;
350 0           return $self->{_changed};
351             }
352              
353             sub _completed {
354 0     0     my $self = shift;
355 0           my $field = shift;
356 0 0         $self->{_completed} = 1 if $field;
357 0           return $self->{_completed};
358             }
359              
360             =head2 collection_name, essential, field, relation, trigger, query_attributes, indexes
361              
362             alias to installed value
363              
364             $collection_name = $document_object->collection_name;
365             $essential = $document_object->essential;# {_id => 1, field1 => 1, field2 => 1}
366              
367             # MongoDBx::Tiny::Document::Field
368             $field = $document_object->field;
369              
370             # MongoDBx::Tiny::Document::Relation
371             $relation = $document_object->relation;
372              
373             $qa = $document_object->query_attributes;
374             $attr = $qa->{$condition}; # condition: single,search
375              
376             $indexes = $document_object->indexes; # arrayref
377              
378             =cut
379              
380             sub collection_name {
381 0     0 1   my $class = shift; # or self
382 0           util_class_attr('COLLECTION_NAME',$class);
383             }
384              
385             sub essential {
386 0     0 1   my $self = shift;
387 0   0       my @essential = util_class_attr('ESSENTIAL',$self) || '_id';
388              
389 0 0         if (ref $essential[0] eq 'ARRAY') {
390 0           @essential = @{$essential[0]};
  0            
391             }
392 0 0         my $ret = @essential ? { map { $_ => 1 } @essential } : {};
  0            
393 0 0         $ret->{_id} = 1 unless $ret->{_id};
394 0           return $ret;
395             }
396              
397             sub field {
398 0     0 1   my $class = shift; # or self
399 0           return util_class_attr('FIELD',$class);
400             }
401              
402             sub relation {
403 0     0 1   my $class = shift; # or self
404 0           return util_class_attr('RELATION',$class);
405             }
406              
407             sub trigger {
408 0     0 1   my $class = shift; # or self
409 0           my $name = shift;
410 0           my $stat = util_class_attr('TRIGGER',$class);
411 0 0         return $stat->{$name} if $name;
412 0           return util_class_attr('TRIGGER',$class);
413             }
414              
415             sub query_attributes {
416 0     0 1   my $class = shift; # or self
417 0           my $condition = shift;
418 0           my $reserved = util_class_attr('QUERY_ATTRIBUTES',$class);
419              
420 0 0         return unless $reserved;
421 0 0         return $reserved->{$condition} if $condition;
422 0           return $reserved;
423             }
424              
425             sub indexes {
426 0     0 1   my $class = shift;
427 0           util_class_attr('INDEXES',$class)
428             }
429              
430             =head2 id
431              
432             returns document value "_id"
433              
434             =cut
435              
436             {
437 1     1   8 no warnings qw(once);
  1         3  
  1         1150  
438             *id = \&_id;
439             }
440              
441 0     0     sub _id { shift->{_id} }
442              
443             =head2 tiny
444              
445             returns MongoDBx::Tiny object
446              
447             =cut
448              
449             sub tiny {
450 0     0 1   my $self = shift;
451 0           my $tiny = $self->{_tiny};
452 0 0         unless ($tiny->connection) {
453 0           $tiny->connect;
454             }
455 0           return $tiny;
456             }
457              
458             =head2 attributes_hashref
459              
460             alias to object_to_document
461              
462             =cut
463              
464 0     0 1   sub attributes_hashref { shift->object_to_document(@_) }
465              
466             =head2 object_to_document
467              
468             $document = $document_object->object_to_document;
469              
470             =cut
471              
472             sub object_to_document {
473             # xxx
474 0     0 1   my $self = shift;
475 0           my $opt = shift;
476 0           my $ret = {};
477              
478 0           for my $field ("_id",$self->field->list) {
479 0           $ret->{$field} = $self->$field();
480             }
481 0           return $ret;
482             }
483              
484             =head2 collection
485              
486             returns MongoDB::Collection
487              
488             $collection = $document_object->collection('collection_name');
489              
490             =cut
491              
492             sub collection {
493 0     0 1   my $self = shift;
494 0           return $self->tiny->collection($self->collection_name);
495             }
496              
497             =head2 update
498              
499              
500             $document_object->field_name('val');
501             $document_object->update;
502              
503             #
504             $document_object->update($document);
505              
506             # only field_name will be changed
507             $document_object->update({ field_name => 'val'});
508              
509             =cut
510              
511             sub update {
512 0     0 1   my $self = shift;
513 0           my $document = shift;
514 0           my $opt = shift;
515 0           $opt->{state} = 'update';
516 0 0 0       if ($document && ! ref $document eq 'HASH') {
517 0           confess 'invalid document';
518             }
519              
520 0           for (keys %{$self->_changed}) {
  0            
521 0           $document->{$_} = $self->$_();
522             }
523              
524 0 0         if (!$document) {
525 0           return;
526             } else {
527 0 0         return unless (keys %$document);
528             }
529              
530 0           my $validator = $self->tiny->validate(
531             $self->collection_name,$document,$opt
532             );
533              
534 0 0         if ($validator->has_error) {
535 0           confess "invalid document: \n" . (Dumper $validator->errors);
536             }
537 0 0         unless ($opt->{no_trigger}) {
538 0           $self->call_trigger('before_update',$opt);
539             }
540              
541             $self->collection->update(
542 0           {'_id' => $self->id},{ '$set' => $document }
543             );
544 0           $self->$_($document->{$_}) for keys %$document;
545 0           $self->{_changed} = {};
546              
547 0 0         unless ($opt->{no_trigger}) {
548 0           $self->call_trigger('after_update',$opt);
549             }
550              
551 0           return $self;
552             }
553              
554             =head2 remove
555              
556             $document_object->remove;
557              
558             =cut
559              
560             sub remove {
561 0     0 1   my $self = shift;
562 0   0       my $opt = shift || {};
563              
564 0 0         unless ($opt->{no_trigger}) {
565 0           $self->call_trigger('before_remove', $opt);
566             }
567              
568 0           my $collection = $self->collection;
569 0           $collection->remove({'_id' => $self->id});
570              
571 0 0         unless ($opt->{no_trigger}) {
572 0           $self->call_trigger('after_remove', $opt);
573             }
574              
575 0           bless $self, __PACKAGE__ . '::REMOVED';
576              
577 0           return 1;
578             }
579              
580 0     0     sub DESTROY {
581             # xxx
582             }
583              
584             package MongoDBx::Tiny::Document::Accessor;
585 1     1   9 use strict;
  1         8  
  1         147  
586             use overload
587 1         14 '""' => \&data,
588 1     1   9 'fallback' => 1;
  1         3  
589              
590             sub new {
591 0     0     my $class = shift;
592 0   0       my $field_info = shift || {}; # { field1 => [sub,sub], field2 => [sub,sub] }
593 0           bless { _data => $field_info }, $class;
594             }
595              
596             sub add {
597 0     0     my $self = shift;
598 0           my ($name,$val) = @_;
599 0           $self->{_data}->{$name} = $val;
600             }
601              
602             sub data {
603 0     0     my $self = shift;
604 0           return $self->{_data};
605             }
606              
607             sub list {
608 0     0     my $self = shift;
609 0           keys %{$self->{_data}};
  0            
610             }
611              
612             sub get {
613 0     0     my $self = shift;
614 0           my $name = shift;
615 0           $self->{_data}->{$name};
616             }
617              
618             =head2 MongoDBx::Tiny::Document::Field
619              
620             my $field = $document_object->field;
621            
622             my $attr = $document_object->get('field_name');
623             $attr->{name};
624             $attr->{callback};
625            
626             my @field_names = $field->list;
627            
628             my @default_fields = $field->list('DEFAULT');
629             my @required_fields = $field->list('REQUIRED')
630             my @oid_fields = $field->list('OID');
631              
632             =cut
633              
634             package MongoDBx::Tiny::Document::Field;
635 1     1   360 use base qw(MongoDBx::Tiny::Document::Accessor);
  1         2  
  1         914  
636              
637             sub add {
638 0     0     my $self = shift;
639 0           my ($name,$val) = @_;
640 0           for (@{$val}) {
  0            
641             # { name => 'name', callback => sub{} }
642 0 0         unless (defined $_->{name} ) {
643 0           die q/invalid field attribute: no name/;
644             }
645 0 0         unless (ref $_->{callback} eq 'CODE') {
646 0           die q/invalid field attribute: invalid callback: / . $_->{name};
647             }
648              
649 0   0       my $req = $self->{_GROUP}->{$_->{name}} || [];
650 0           push @$req, $name;
651 0           $self->{_GROUP}->{$_->{name}} = $req;
652             }
653 0           $self->SUPER::add($name,$val);
654             }
655              
656             sub list {
657 0     0     my $self = shift;
658 0           my $name = shift;
659 0 0         if ($name) {
660 0   0       my $req = $self->{_GROUP}->{$name} || [];
661 0           return @$req;
662             }
663 0           $self->SUPER::list;
664             }
665              
666             =head2 MongoDBx::Tiny::Document::Relation;
667              
668             my $relation = $document_object->relation;
669             my @relations = $relation->list;
670              
671             =cut
672              
673             package MongoDBx::Tiny::Document::Relation;
674 1     1   7 use base qw(MongoDBx::Tiny::Document::Accessor);
  1         3  
  1         489  
675              
676             1;
677             __END__
678              
679             =head1 AUTHOR
680              
681             Naoto ISHIKAWA, C<< <toona at seesaa.co.jp> >>
682              
683             =head1 LICENSE AND COPYRIGHT
684              
685             Copyright 2013 Naoto ISHIKAWA.
686              
687             This program is free software; you can redistribute it and/or modify it
688             under the terms of either: the GNU General Public License as published
689             by the Free Software Foundation; or the Artistic License.
690              
691             See http://dev.perl.org/licenses/ for more information.
692              
693              
694             =cut