File Coverage

blib/lib/MongoDBx/Tiny.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package MongoDBx::Tiny;
2              
3 1     1   22565 use 5.006;
  1         4  
  1         39  
4 1     1   7 use strict;
  1         2  
  1         34  
5 1     1   5 use warnings;
  1         6  
  1         72  
6              
7             =head1 NAME
8              
9             MongoDBx::Tiny - Simple Mongo ORM for Perl
10              
11             =head1 VERSION
12              
13             Version 0.04
14              
15             =cut
16              
17             our $VERSION = '0.04';
18              
19             =head1 SYNOPSIS
20              
21             # --------------------
22             package My::Data;
23              
24             use MongoDBx::Tiny;
25              
26             CONNECT_INFO host => 'localhost', port => 27017;
27             DATABASE_NAME 'my_data';
28              
29             # --------------------
30             package My::Data::Foo;
31              
32             use MongoDBx::Tiny::Document;
33              
34             COLLECTION_NAME 'foo';
35              
36             ESSENTIAL qw/code/;
37             FIELD 'code', INT, LENGTH(10), DEFAULT('0'), REQUIRED;
38             FIELD 'name', STR, LENGTH(30), DEFAULT('noname');
39              
40             # --------------------
41             package main;
42              
43             my $tiny = My::Data->new;
44             $tiny->insert(foo => { code => 123, name => "foo_123"}) or die $!;
45             my $foo = $tiny->single(foo => { code => 123});
46             $foo->name('foo_321');
47             $foo->update;
48             $foo->remove;
49              
50             =cut
51              
52 1     1   596 use MongoDB;
  0            
  0            
53             use Carp qw/carp confess/;
54             use Data::Dumper;
55             use MongoDBx::Tiny::Util;
56             use Params::Validate ();
57             use Scalar::Util qw(blessed);
58              
59              
60             sub import {
61             my $caller = (caller(0))[0];
62             {
63             no strict 'refs';
64             push @{"${caller}::ISA"}, __PACKAGE__;
65             }
66             strict->import;
67             warnings->import;
68             __PACKAGE__->export_to_level(1, @_);
69             }
70              
71             require Exporter;
72             use base qw/Exporter/;
73             our @EXPORT = qw/CONNECT_INFO DATABASE_NAME LOAD_PLUGIN/;
74             our $_CONNECT_INFO;
75             our $_DATABASE_NAME;
76              
77             =head1 EXPORT
78              
79             A list of functions that can be exported.
80              
81             =head2 CONNECT_INFO
82              
83             CONNECT_INFO host => 'localhost', port => 27017;
84              
85             =head2 DATABASE_NAME
86              
87             DATABASE_NAME 'my_data';
88              
89             =head2 LOAD_PLUGIN
90              
91             LOAD_PLUGIN 'One'; # MongoDBx::Tiny::Plugin::One
92             LOAD_PLUGIN 'Two';
93             LOAD_PLUGIN '+Class::Name';
94              
95             =cut
96              
97             {
98             no warnings qw(once);
99             *CONNECT_INFO = \&install_connect_info;
100             *DATABASE_NAME = \&install_database_name;
101             *LOAD_PLUGIN = \&install_plugin;
102             }
103              
104             sub install_connect_info { util_class_attr('CONNECT_INFO', @_) }
105              
106             sub install_database_name { util_class_attr('DATABASE_NAME',@_) }
107              
108             sub install_plugin {
109             my $class = (caller(0))[0];
110             $class->load_plugin(shift)
111             }
112              
113              
114             =head1 SUBROUTINES/METHODS
115              
116             =head2 new
117              
118             my $tiny = My::Data->new();
119              
120             # or you can specify connect_info, database_name.
121             my $tiny = My::Data->new({
122             connect_info => [ host => 'localhost', port => 27017 ],
123             database_name => 'my_data',
124             });
125              
126             =cut
127              
128             sub new {
129             my $class = shift;
130             my $opt = shift;
131             if (ref $opt->{connect_info} eq 'ARRAY') {
132             $class->install_connect_info($opt->{connect_info});
133             }
134             if ($opt->{database_name} ) {
135             $class->install_database_name($opt->{database_name});
136             }
137             my $self = bless{},$class;
138             eval { $self->connect; };
139              
140             if ($@) {
141             Carp::carp $@;
142             return;
143             }
144              
145             return $self;
146             }
147              
148             =head2 get_connection
149              
150             returns MongoDB::Connection. you can override how to get connection object.
151              
152             sub get_connection {
153             my $class = shift;
154              
155             return MongoDB::Connection->new(@{$class->CONNECT_INFO}) if !$ENV{PLACK_ENV};
156              
157             my $key = 'some_key';
158             if (in_scope_container() and my $con = scope_container($key)) {
159             return $con;
160             } else {
161             my $con = MongoDB::Connection->new(@{$class->CONNECT_INFO});
162             scope_container($key, $con) if in_scope_container();
163             return $con;
164             }
165             }
166              
167              
168             =cut
169              
170             sub get_connection {
171             my $class = shift;
172             return MongoDB::Connection->new(@{$class->connect_info});
173             }
174              
175             =head2 connect_info, database_name
176              
177             alias to installed value
178              
179             my $connect_info = $tiny->connect_info;
180              
181             my $database_name = $tiny->database_name;
182              
183             =cut
184              
185             sub connect_info {
186             my $class = shift; # or self
187             util_class_attr('CONNECT_INFO',$class);
188             }
189              
190             sub database_name {
191             my $class = shift; # or self
192             util_class_attr('DATABASE_NAME',$class);
193             }
194              
195             =head2 connection, database
196              
197             alias to connection and database object.
198              
199             my $connection = $tiny->connection; # MongoDB::Connection object
200              
201             my $database = $tiny->database; # MongoDB::Database object
202              
203             =cut
204              
205              
206             sub connection { shift->{connection} }
207              
208             sub database { shift->{database} }
209              
210             =head2 cursor_class, validator_class, gridfs_class
211              
212             override if you want.
213              
214             MongoDBx::Tiny::Cursor
215            
216             MongoDBx::Tiny::Validator
217            
218             MongoDBx::Tiny::GridFS
219              
220             =cut
221              
222             sub cursor_class { 'MongoDBx::Tiny::Cursor' }
223              
224             sub validator_class { 'MongoDBx::Tiny::Validator' }
225              
226             sub gridfs_class { 'MongoDBx::Tiny::GridFS' }
227              
228             =head2 collection
229              
230             returns MongoDB::Collection
231              
232             $collection = $tiny->collection('collection_name')
233              
234             =cut
235              
236             sub collection {
237             my $self = shift;
238             my $name = shift or confess q|no collection name|;
239             my $opt = shift || {no_cache => 0};
240             if ($opt->{no_cache}) {
241             if ($self->database->can('get_collection')) {
242             return $self->database->get_collection($name);
243             } else {
244             return $self->database->$name();
245             }
246             } else {
247             my $cache = $self->{collection};
248             return $cache->{$name} if $cache->{$name};
249             return ($self->{collection}->{$name} = $self->database->can('get_collection') ?
250             $self->database->get_collection($name) :
251             $self->database->$name());
252             }
253             }
254              
255             =head2 connect / disconnect
256              
257             just (re)connect & disconnect
258              
259             =cut
260              
261             sub connect {
262             my $self = shift;
263             $self->disconnect;
264              
265             my $connection = $self->get_connection;
266             my $db_name = $self->database_name;
267             my $database = $connection->can('get_database') ?
268             $connection->get_database($db_name) :
269             $connection->$db_name();
270             $self->{connection} = $connection;
271             $self->{database} = $database;
272             $self->{gridfs} = undef;
273              
274             return $self->{connection};
275             }
276              
277             sub disconnect {
278             my $self = shift;
279             for (qw(connection database gridfs collection)) {
280             delete $self->{$_};
281             }
282             return 1;
283             }
284              
285              
286             =head2 insert,create
287              
288             $document_object = $tiny->insert('collection_name' => $document);
289              
290             =cut
291              
292             {
293             no warnings qw(once);
294             *create = \&insert;
295             }
296              
297             sub insert {
298             my $self = shift;
299             my $c_name = shift or confess q/no collection name/;
300             my $document = shift or confess q/no document/;
301              
302             my $opt = shift;
303             $opt->{state} = 'insert';
304              
305             my $validator = $self->validate($c_name,$document,$opt);
306              
307             if ($validator->has_error) {
308             confess "invalid document: \n" . (Dumper $validator->errors);
309             }
310              
311             my $d_class = $self->document_class($c_name);
312             unless ($opt->{no_trigger}) {
313             $d_class->call_trigger('before_insert',$self,$document,$opt) ;
314             }
315              
316             my $collection = $self->collection($c_name);
317             my $id = $collection->insert($document);
318             my $object = $self->single($c_name,$id);
319             unless ($opt->{no_trigger}) {
320             $d_class->call_trigger('after_insert',$object,$opt);
321             }
322             return $object;
323             }
324              
325             =head2 single
326              
327             returns MongoDBx::Tiny::Document object.
328              
329             $document_object = $tiny->single('collection_name' => $MongoDB_oid_object);
330            
331             $tiny->single('collection_name' => $oid_text);
332            
333             $query = { field => $val };
334             $tiny->single('collection_name' => $query);
335              
336             =cut
337              
338             sub single {
339             #xxx
340             # 4ff19d717bcc56834b000000
341             # tiny->single("sfa"); # return first value
342             my $self = shift;
343             my $c_name = shift;
344              
345             my $collection = $self->collection($c_name);
346             my $document;
347             my $d_class = $self->document_class($c_name);
348              
349             my $essential = $d_class->essential; #
350              
351             my ($proto) = shift;
352             unless (ref $proto eq 'HASH') {
353             $proto = { _id => "$proto" };
354             }
355             $proto = util_to_oid($proto,'_id',$d_class->field->list('OID'));
356              
357             my $reserved= $d_class->query_attributes('single');
358             if ($reserved && ( my @attr = keys %$reserved)) {
359             $proto->{$_} ||= $reserved->{$_} for @attr;
360             }
361             $document = $collection->find_one($proto,$essential);
362              
363             # # needed?
364             # elsif (scalar @_ >= 2) {
365             # my %query = @_;
366             # $document = $collection->find_one(\%query,$essential);
367             # }
368             return unless $document;
369             $self->document_to_object($c_name,$document);
370             }
371              
372             =head2 search
373              
374             returns MongoDBx::Tiny::Cursor object.
375              
376             $query = { field => $val };
377             $cursor = $tiny->search('collection_name' => $query);
378             while (my $object = $cursor->next) {
379             # warn $object->id;
380             }
381            
382             # list context
383             @object = $tiny->search('collection_name' => $query);
384              
385             =cut
386              
387             sub search {
388             my $self = shift;
389             # xxx
390             my $c_name = shift;
391             my $collection = $self->collection($c_name);
392             my $d_class = $self->document_class($c_name);
393             my $essential = $d_class->essential; #
394             my $query = shift;
395             my @operation = @_;
396              
397             $query = util_to_oid($query,'_id',$d_class->field->list('OID'));
398             my $reserved= $d_class->query_attributes('search');
399             if ($reserved && ( my @attr = keys %$reserved)) {
400             $query->{$_} ||= $reserved->{$_} for @attr;
401             }
402              
403             my $cursor = $collection->find($query)->fields($essential);
404             if (wantarray) {
405             return map { $self->document_to_object($c_name,$_) } $cursor->all;
406             } else {
407             eval "require " . $self->cursor_class;
408             return $self->cursor_class->new(
409             tiny => $self, c_name => $c_name,cursor => $cursor
410             );
411             }
412             }
413              
414             =head2 update
415              
416             $tiny->update('collection_name',$query,$document);
417              
418             =cut
419              
420             sub update {
421             my $self = shift;
422             # xxx
423             my $c_name = shift || confess q/no collection name/;;
424             my $query = shift || confess q/no query/;
425             my $document = shift;
426             my $opt = shift;
427             return unless $document;
428             $opt->{state} = 'update';
429              
430             my $validator = $self->validate(
431             $c_name,$document,$opt
432             );
433            
434             if ($validator->has_error) {
435             confess "invalid document: \n" . (Dumper $validator->errors);
436             }
437             my $d_class = $self->document_class($c_name);
438              
439             my @object; # xxx
440             if (!$opt->{no_trigger} and $d_class->trigger('before_update')) {
441             my $cursor = $self->search($c_name,$query);
442             while (my $object = $cursor->next) {
443             push @object,$object;
444             $object->call_trigger('before_update',$self,$opt);
445             }
446             }
447              
448             my $collection = $self->collection($c_name);
449             $collection->update($query,{ '$set' => $document },{ multiple => 1 });
450              
451             if (!$opt->{no_trigger} and $d_class->trigger('after_update')) {
452             for my $object (@object) {
453             $object->call_trigger('after_update',$self,$opt);
454             }
455             }
456            
457             # tiny->remove('foo',{ code => 111 });
458             }
459              
460             =head2 remove
461              
462              
463             $tiny->remove('collection_name',$query);
464              
465             =cut
466              
467             sub remove {
468             my $self = shift;
469             # xxx
470             my $c_name = shift || confess q/no collection name/;;
471             my $query = shift || confess q/no query/;
472             my $opt = shift || {};
473              
474             my $d_class = $self->document_class($c_name);
475            
476             my @object; # xxx
477             if (!$opt->{no_trigger} and $d_class->trigger('before_remove')) {
478             my $cursor = $self->search($c_name,$query);
479             while (my $object = $cursor->next) {
480             push @object,$object;
481             $object->call_trigger('before_remove',$object,$opt);
482             }
483             }
484              
485             my $collection = $self->collection($c_name);
486             $collection->remove($query);
487              
488             if (!$opt->{no_trigger} and $d_class->trigger('after_remove')) {
489             for my $object (@object) {
490             $object->call_trigger('after_remove',$object,$opt);
491             }
492             }
493             }
494              
495             =head2 count
496              
497             $count_num = $tiny->count('collection_name',$query);
498              
499             =cut
500              
501             sub count {
502             my $self = shift;
503             my $c_name = shift;
504             my $collection = $self->collection($c_name);
505             my $d_class = $self->document_class($c_name);
506             return $collection->count(shift);
507             }
508              
509             =head2 document_to_object
510              
511             $document_object = $tiny->document_to_object('collection_name',$document);
512              
513             =cut
514              
515             sub document_to_object {
516             my $self = shift;
517             my $c_name = shift or confess q/no collecion name/;
518             my $document = shift or confess q/no document/;
519             confess q/no id/ unless $document->{_id};
520              
521             my $d_class = $self->document_class($c_name);
522              
523             return $d_class->new($document,$self);
524             }
525              
526             =head2 validate
527              
528             $validator = $tiny->validate('collecion_name',$document,$opt);
529              
530             my $validator = $tiny->validate(
531             'foo',
532             { code => 123, name => "foo_123"},
533             { state => 'insert' }
534             );
535             my $foo1 = $tiny->insert(
536             'foo',
537             $validator->document,
538             { state => 'insert', no_validate => 1 }
539             );
540             # erros: [{ field => 'field1', code => 'errorcode', message => 'message1' },,,]
541             my @erros = $validator->erros;
542            
543             my @fields = $validator->errors('field');
544             my @error_code = $validator->errors('code');
545             my @error_message = $validator->errors('message');
546              
547             =cut
548              
549             sub validate {
550             my $self = shift;
551             my $c_name = shift or confess q/no collecion_name/;
552             my $document = shift or confess q/no document/;
553             my $opt = shift;
554              
555             Params::Validate::validate_with(
556             params => $opt,
557             spec => {
558             state => 1,# insert,update
559             no_trigger => 0,
560             no_validate => 0,
561             },
562             allow_extra => 1,
563             );
564            
565             $opt->{tiny} = $self;
566             eval "require " . $self->validator_class;
567             my $validator = $self->validator_class->new(
568             $c_name,
569             $document,
570             $self,
571             );
572            
573             return $validator->check($opt);
574             }
575              
576             =head2 gridfs
577              
578             returns MongoDBx::Tiny::GridFS
579              
580             $gridfs = $tiny->gridfs();
581            
582             $gridfs = $tiny->gridfs({database => $mongo_databse_object });
583            
584             $gridfs = $tiny->gridfs({fields => 'other_filename' });
585              
586             my $gridfs = $tiny->gridfs;
587             $gridfs->put('/tmp/foo.txt', {"filename" => 'foo.txt' });
588             my $foo_txt = $gridfs->get({ filename => 'foo.txt' })->slurp;
589            
590             $gridfs->put('/tmp/bar.txt','bar.txt');
591             my $bar_txt = $gridfs->get('bar.txt')->slurp;
592              
593             =cut
594              
595             sub gridfs {
596             my $self = shift;
597             my $opt = shift || {};
598             my %opt = Params::Validate::validate_with(
599             params => $opt,
600             spec => {
601             database => {optional => 1},
602             fields => {optional => 1, default => 'filename'},
603             }
604             );
605              
606             my $database = $opt{database} || $self->database;
607              
608             eval "require " . $self->gridfs_class;
609             return $self->{gridfs} if $self->{gridfs};
610             $self->{gridfs} = $self->gridfs_class->new(
611             $database->get_gridfs,$opt{fields}
612             );
613             }
614              
615              
616             =head2 document_class
617              
618             $document_class_name = $tiny->document_class('collecion_name');
619              
620             =cut
621              
622             sub document_class {
623             my $self = shift;
624             my $c_name = shift or confess q/no collection name/;
625             return util_document_class($c_name,ref $self);
626             }
627              
628             =head2 load_plugin
629              
630             # --------------------
631            
632             package MyDB;
633             use MongoDBx::Tiny;
634            
635             LOAD_PLUGIN('PluginName');
636             LOAD_PLUGIN('+Class::Name');
637            
638             # --------------------
639             package MongoDBx::Tiny::Plugin::PluginName;
640             use strict;
641             use warnings;
642             use utf8;
643            
644             our @EXPORT = qw/function_for_plugin/;
645            
646             sub function_for_plugin {}
647            
648             # --------------------
649            
650             $tiny->function_for_plugin;
651              
652             =cut
653              
654             sub load_plugin {
655             my ($proto, $pkg) = @_;
656            
657             my $class = ref $proto ? ref $proto : $proto;
658             $pkg = $pkg =~ s/^\+// ? $pkg : "MongoDBx::Tiny::Plugin::$pkg";
659             eval "require $pkg ";
660              
661             no strict 'refs';
662             for my $method ( @{"${pkg}::EXPORT"} ) {
663             next if $class->can($method);
664             *{$class . '::' . $method} = $pkg->can($method);
665             }
666             }
667              
668             =head2 process
669              
670             [EXPERIMENTAL]
671              
672             $tiny->process('collecion_name','some',@args);
673              
674             $tiny->process('foo','some',$validator,$arg); # just call Data::Foo::process_some
675            
676             #
677             sub process_foo {
678             my ($class,$tiny,$validator,$arg) = @_;
679             }
680              
681             =cut
682              
683             sub process {
684             # xxx
685             my $self = shift;
686             my $c_name = shift or confess q/no collecion name/;
687             my $method = shift or confess q/no process method name/;
688             my $d_class = $self->document_class($c_name);
689             my $process_method = sprintf "process_%s", $method;
690             $d_class->$process_method($self,@_);
691             }
692              
693             =head2 set_indexes
694              
695             [EXPERIMENTAL]
696              
697             $tiny->set_indexes('collection_name');
698              
699             =cut
700              
701             sub set_indexes {
702             my $self = shift;
703             my $c_name = shift;
704             my $collection = $self->collection($c_name);
705             my $d_class = $self->document_class($c_name);
706             my $indexes = $d_class->indexes || [];
707              
708              
709             my $ns = sprintf "%s.%s", $self->database_name,$c_name;
710              
711             for my $index (@$indexes) {
712              
713             my ($field,$index_opt,$opt) = @$index;
714             require Tie::IxHash;
715             if (ref $field eq 'ARRAY') {
716             $field = Tie::IxHash->new(@$field);
717             }
718             my $index_target = ref $field ? $field : { $field => 1 };
719             my ($index_exists) = $self->collection('system.indexes')->find({ns => $ns,key => $index_target})->all;
720            
721             if (!$index_exists) {
722             $collection->ensure_index($index_target,$index_opt);
723             }
724             }
725             }
726              
727             =head2 unset_indexes
728              
729             [EXPERIMENTAL]
730              
731             # drop indexes without "_id";
732             $tiny->unset_indexes('collection_name');
733              
734             =cut
735              
736             sub unset_indexes {
737             my $self = shift;
738             my $c_name = shift;
739             my $collection = $self->collection($c_name);
740              
741             #> db.system.indexes.find({ns:"my_data.foo",key:{$ne : {"_id":1}}});
742             my $ns = sprintf "%s.%s", $self->database_name,$c_name;
743             my @index = $self->collection('system.indexes')->find({ns => $ns,key => { '$ne' => {"_id" => 1}}})->all;
744             for (@index) {
745             $collection->drop_index($_->{key});
746             }
747             }
748              
749             sub DESTROY {
750             # xxx
751             }
752              
753              
754             1;
755              
756             __END__