File Coverage

blib/lib/MongoDBx/Tiny/Document.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package MongoDBx::Tiny::Document;
2              
3 1     1   1770 use 5.006;
  1         4  
  1         34  
4 1     1   5 use strict;
  1         1  
  1         25  
5 1     1   4 use warnings;
  1         2  
  1         34  
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   4 use Data::Dumper;
  1         2  
  1         45  
76 1     1   5 use Scalar::Util qw(blessed);
  1         1  
  1         266  
77 1     1   439 use Class::Trigger;
  0            
  0            
78             use Carp qw/carp confess/;
79             use MongoDBx::Tiny::Util;
80             use Params::Validate;
81              
82             use overload
83             '""' => \&id,
84             'fallback' => 1;
85              
86             sub import {
87             my $class = shift || __PACKAGE__;
88             my $caller = (caller(0))[0];
89             {
90             no strict 'refs';
91             push @{"${caller}::ISA"}, $class;
92             }
93             strict->import;
94             warnings->import;
95             __PACKAGE__->export_to_level(1, @_);
96             if (__PACKAGE__ ne $class) {
97             $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             use MongoDBx::Tiny::Attributes;
175             use MongoDBx::Tiny::Relation;
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             no warnings qw(once);
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             sub install_collection_name { util_class_attr('COLLECTION_NAME',@_) }
196              
197             sub install_essential{ util_class_attr('ESSENTIAL',@_) }
198              
199             sub install_field {
200             my ($proto) = shift;
201             my ($class,$stat) = util_guess_class($proto);
202             my $name;
203             if ($stat->{caller}) {
204             $name = $proto;
205             }
206              
207              
208             my $attr = 'FIELD';
209              
210             my $field_obj = util_class_attr($attr,$class) ||
211             MongoDBx::Tiny::Document::Field->new;
212              
213             Carp::croak q/FIELD needs attributes/ unless @_;
214              
215             if (@_) {
216             my (@type) = @_;
217             $field_obj->add($name,\@type);
218             util_class_attr($attr,$class,$field_obj);
219              
220             unless ($class->can($name)) {
221             my $accessor = sub {
222             my $self = shift;
223             unless ($self->_completed){
224             my $essential = $self->essential;
225             if (!$essential->{$name}) {
226             my @not_complete = grep { !$essential->{$_}} $self->field->list;
227             my $doc = $self->collection->find_one(
228             {_id => $self->id},{ map { $_ => 1 } @not_complete }
229             );
230             for (@not_complete) {
231             $self->{$_} = $doc->{$_};
232             }
233             $self->_completed(1);
234             }
235             }
236              
237             if(@_ >= 1) {
238             $self->_changed($name);
239             }
240             if(@_ == 1) {
241             return $self->{$name} = $_[0];
242             } elsif(@_ > 1) {
243             return $self->{$name} = [@_];
244             } else {
245             return $self->{$name};
246             }
247             };
248             {
249             no strict 'refs';
250             *{"${class}::${name}"} = $accessor;
251             }
252             }
253             }
254             return $field_obj;
255             }
256              
257             sub install_relation {
258             my $proto = shift;
259              
260             my ($class,$stat) = util_guess_class($proto);
261              
262             my $c_name;
263             if ($stat->{caller}) {
264             $c_name = $proto;
265             } else {
266             $c_name = shift;
267             }
268              
269             my $attr = 'RELATION';
270              
271             my $relation = util_class_attr($attr,$class) ||
272             MongoDBx::Tiny::Document::Relation->new;
273             if (@_) {
274             my ($clause) = @_;
275             $relation->add($c_name => [$clause]);
276             util_class_attr($attr,$class,$relation);
277              
278             unless ($class->can($c_name)) {
279             {
280             no strict 'refs';
281             *{$class . "::" . $c_name} = sub {
282             my $self = shift;
283             $clause->($self,$c_name);
284             }
285             }
286             }
287             }
288             return $relation;
289             }
290              
291             sub install_trigger {
292             my ($proto) = shift;
293             my ($class,$stat) = util_guess_class($proto);
294             my $name;
295             if ($stat->{caller}) {
296             $name = $proto;
297             }
298             if(@_) {
299             my $trigger = util_class_attr('TRIGGER',$class);
300             $trigger->{$name} ++;
301             util_class_attr('TRIGGER',$class,$trigger);
302             }
303             return $class->add_trigger($name,@_);
304             }
305              
306             sub install_query_attributes{ util_class_attr('QUERY_ATTRIBUTES',@_) }
307              
308             sub install_index {
309             my ($proto) = shift;
310             my ($class,$stat) = util_guess_class($proto);
311             my $name;
312             if ($stat->{caller}) {
313             $name = $proto;
314             }
315              
316             my $tmp;
317             if ($name) {
318             my ($index_opt,$opt) = @_;
319             $tmp = util_class_attr('INDEXES') || [];
320             push @$tmp,[ $name,$index_opt,$opt];
321             }
322              
323             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             my $class = shift;
337             my $document = shift or confess q/no document/;
338             my $tiny = shift or confess q/no tiny/;
339             my $self = bless $document , $class;
340             $self->{_tiny} = $tiny;
341             $self->{_changed} = {}; # field is changed or not
342             $self->{_completed} = 0; # all fields are fetched or not.
343             return $self;
344             }
345              
346             sub _changed {
347             my $self = shift;
348             my $field = shift;
349             $self->{_changed}->{$field} = 1 if $field;
350             return $self->{_changed};
351             }
352              
353             sub _completed {
354             my $self = shift;
355             my $field = shift;
356             $self->{_completed} = 1 if $field;
357             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             my $class = shift; # or self
382             util_class_attr('COLLECTION_NAME',$class);
383             }
384              
385             sub essential {
386             my $self = shift;
387             my @essential = util_class_attr('ESSENTIAL',$self) || '_id';
388              
389             if (ref $essential[0] eq 'ARRAY') {
390             @essential = @{$essential[0]};
391             }
392             my $ret = @essential ? { map { $_ => 1 } @essential } : {};
393             $ret->{_id} = 1 unless $ret->{_id};
394             return $ret;
395             }
396              
397             sub field {
398             my $class = shift; # or self
399             return util_class_attr('FIELD',$class);
400             }
401              
402             sub relation {
403             my $class = shift; # or self
404             return util_class_attr('RELATION',$class);
405             }
406              
407             sub trigger {
408             my $class = shift; # or self
409             my $name = shift;
410             my $stat = util_class_attr('TRIGGER',$class);
411             return $stat->{$name} if $name;
412             return util_class_attr('TRIGGER',$class);
413             }
414              
415             sub query_attributes {
416             my $class = shift; # or self
417             my $condition = shift;
418             my $reserved = util_class_attr('QUERY_ATTRIBUTES',$class);
419              
420             return unless $reserved;
421             return $reserved->{$condition} if $condition;
422             return $reserved;
423             }
424              
425             sub indexes {
426             my $class = shift;
427             util_class_attr('INDEXES',$class)
428             }
429              
430             =head2 id
431              
432             returns document value "_id"
433              
434             =cut
435              
436             {
437             no warnings qw(once);
438             *id = \&_id;
439             }
440              
441             sub _id { shift->{_id} }
442              
443             =head2 tiny
444              
445             returns MongoDBx::Tiny object
446              
447             =cut
448              
449             sub tiny {
450             my $self = shift;
451             my $tiny = $self->{_tiny};
452             unless ($tiny->connection) {
453             $tiny->connect;
454             }
455             return $tiny;
456             }
457              
458             =head2 attributes_hashref
459              
460             alias to object_to_document
461              
462             =cut
463              
464             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             my $self = shift;
475             my $opt = shift;
476             my $ret = {};
477              
478             for my $field ("_id",$self->field->list) {
479             $ret->{$field} = $self->$field();
480             }
481             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             my $self = shift;
494             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             my $self = shift;
513             my $document = shift;
514             my $opt = shift;
515             $opt->{state} = 'update';
516             if ($document && ! ref $document eq 'HASH') {
517             confess 'invalid document';
518             }
519              
520             for (keys %{$self->_changed}) {
521             $document->{$_} = $self->$_();
522             }
523              
524             if (!$document) {
525             return;
526             } else {
527             return unless (keys %$document);
528             }
529              
530             my $validator = $self->tiny->validate(
531             $self->collection_name,$document,$opt
532             );
533              
534             if ($validator->has_error) {
535             confess "invalid document: \n" . (Dumper $validator->errors);
536             }
537             unless ($opt->{no_trigger}) {
538             $self->call_trigger('before_update',$opt);
539             }
540              
541             $self->collection->update(
542             {'_id' => $self->id},{ '$set' => $document }
543             );
544             $self->$_($document->{$_}) for keys %$document;
545             $self->{_changed} = {};
546              
547             unless ($opt->{no_trigger}) {
548             $self->call_trigger('after_update',$opt);
549             }
550              
551             return $self;
552             }
553              
554             =head2 remove
555              
556             $document_object->remove;
557              
558             =cut
559              
560             sub remove {
561             my $self = shift;
562             my $opt = shift || {};
563              
564             unless ($opt->{no_trigger}) {
565             $self->call_trigger('before_remove', $opt);
566             }
567              
568             my $collection = $self->collection;
569             $collection->remove({'_id' => $self->id});
570              
571             unless ($opt->{no_trigger}) {
572             $self->call_trigger('after_remove', $opt);
573             }
574              
575             bless $self, __PACKAGE__ . '::REMOVED';
576              
577             return 1;
578             }
579              
580             sub DESTROY {
581             # xxx
582             }
583              
584             package MongoDBx::Tiny::Document::Accessor;
585             use strict;
586             use overload
587             '""' => \&data,
588             'fallback' => 1;
589              
590             sub new {
591             my $class = shift;
592             my $field_info = shift || {}; # { field1 => [sub,sub], field2 => [sub,sub] }
593             bless { _data => $field_info }, $class;
594             }
595              
596             sub add {
597             my $self = shift;
598             my ($name,$val) = @_;
599             $self->{_data}->{$name} = $val;
600             }
601              
602             sub data {
603             my $self = shift;
604             return $self->{_data};
605             }
606              
607             sub list {
608             my $self = shift;
609             keys %{$self->{_data}};
610             }
611              
612             sub get {
613             my $self = shift;
614             my $name = shift;
615             $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             use base qw(MongoDBx::Tiny::Document::Accessor);
636              
637             sub add {
638             my $self = shift;
639             my ($name,$val) = @_;
640             for (@{$val}) {
641             # { name => 'name', callback => sub{} }
642             unless (defined $_->{name} ) {
643             die q/invalid field attribute: no name/;
644             }
645             unless (ref $_->{callback} eq 'CODE') {
646             die q/invalid field attribute: invalid callback: / . $_->{name};
647             }
648              
649             my $req = $self->{_GROUP}->{$_->{name}} || [];
650             push @$req, $name;
651             $self->{_GROUP}->{$_->{name}} = $req;
652             }
653             $self->SUPER::add($name,$val);
654             }
655              
656             sub list {
657             my $self = shift;
658             my $name = shift;
659             if ($name) {
660             my $req = $self->{_GROUP}->{$name} || [];
661             return @$req;
662             }
663             $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             use base qw(MongoDBx::Tiny::Document::Accessor);
675              
676             1;
677             __END__