File Coverage

blib/lib/Mojo/Util/Collection.pm
Criterion Covered Total %
statement 206 245 84.0
branch 35 46 76.0
condition 13 29 44.8
subroutine 49 56 87.5
pod 40 40 100.0
total 343 416 82.4


line stmt bran cond sub pod time code
1             package Mojo::Util::Collection;
2 32     32   3962869 use Mojo::Base -base;
  32         381966  
  32         240  
3              
4 32     32   8653 use Exporter 'import';
  32         72  
  32         1224  
5 32     32   16987 use Mojo::Util::Model;
  32         135  
  32         345  
6              
7             our @EXPORT_OK = qw(
8             collect
9             );
10              
11             our $VERSION = '0.0.19';
12              
13 32     32   3068 use List::Util;
  32         89  
  32         2471  
14 32     32   221 use Scalar::Util qw(blessed);
  32         72  
  32         1990  
15              
16 32     32   20603 use Mojo::Util::Collection::Comparator;
  32         142  
  32         221  
17 32     32   17911 use Mojo::Util::Collection::Formatter;
  32         252  
  32         311  
18 32     32   1454 use Mojo::Util::Model;
  32         118  
  32         206  
19              
20              
21             has 'comparator' => sub {
22             return Mojo::Util::Collection::Comparator->new;
23             };
24              
25             has 'formatter' => sub {
26             return Mojo::Util::Collection::Formatter->new;
27             };
28              
29             has 'index' => 0;
30              
31             has 'items' => sub { [] };
32              
33             has 'limit' => 10;
34              
35             has 'model' => sub {
36             return Mojo::Util::Model->new;
37             };
38              
39             has 'objects' => sub {
40             my $self = shift;
41              
42             my @objects = map { $self->newObject(%{$_ || {}}) } @{ $self->items || [] };
43              
44             return \@objects;
45             };
46              
47             has 'pager' => sub { {} };
48              
49              
50             =head2 add
51              
52             Add an object
53              
54             Returns:
55             C of C objects
56              
57             =cut
58              
59             sub add {
60 6     6 1 219827 my ($self, $object) = @_;
61              
62 6 100       19 if (ref($object) eq 'ARRAY') {
63 1         6 $self->add($_) for (@$object);
64              
65 1         3 return $self;
66             }
67              
68 5         6 my @objects = @{ $self->objects };
  5         11  
69              
70 5 100       29 if (ref($object) eq 'HASH') {
    50          
71 3         30 push(@objects, $self->newObject(%$object));
72             } elsif (blessed($object)) {
73 2 100       11 if ($object->isa('Mojo::Util::Collection')) {
    50          
74 1         2 push(@objects, @{ $object->objects });
  1         4  
75             } elsif ($object->isa('Mojo::Util::Model')) {
76 1         2 push(@objects, $object);
77             } else {
78 0         0 warn "Can't add object of type " . ref($object);
79             }
80             } else {
81 0         0 warn "Can't add object of type " . ref($object);
82             }
83              
84 5         34 $self->objects(\@objects);
85              
86 5         39 return $self;
87             }
88              
89             =head2 as
90              
91             Set model for collection
92              
93             =cut
94              
95             sub as {
96 34     34 1 575 my ($self, $as) = @_;
97              
98 34         155 $self->model($as);
99              
100 34         313 return $self;
101             }
102              
103             =head2 asOptions
104              
105             Return an array ref containing [{ value => $value, label => $label }, ...]
106              
107             =cut
108              
109             sub asOptions {
110 2     2 1 153389 my $self = shift;
111              
112 2         10 return $self->formatter->asOptions($self->objects, @_);
113             }
114              
115             =head2 avg
116              
117             Return the average value for given $field
118              
119             =cut
120              
121             sub avg {
122 0     0 1 0 my ($self, $field) = @_;
123 0         0 my $values = $self->lists($field);
124              
125 0         0 return List::Util::sum(@$values) / scalar(@$values);
126             }
127              
128             =head2 collect
129              
130             Instantiate a new collection
131              
132             =cut
133              
134             sub collect {
135 2     2 1 172890 my $items = shift;
136              
137 2         20 return Mojo::Util::Collection->new({ items => $items });
138             }
139              
140             =head2 count
141              
142             Return the size of objects
143              
144             =cut
145              
146             sub count {
147 40     40 1 756375 return (scalar(@{ shift->objects }));
  40         118  
148             }
149              
150             =head2 each
151              
152             Map through each object and call the callback
153              
154             =cut
155              
156             sub each {
157 1     1 1 176921 my ($self, $callback) = @_;
158              
159 1         2 $callback->($_) for (@{ $self->objects });
  1         5  
160             }
161              
162             =head2 exclude
163              
164             Exclude objects that doesn't match criteria
165              
166             Returns:
167             C of C objects
168              
169             =cut
170              
171             sub exclude {
172 5     5 1 2215 my ($self, $args) = @_;
173              
174             my $collection = $self->filter(sub {
175 19     19   29 my $object = shift;
176              
177 19         108 return List::Util::notall { $self->comparator->verify($object->get($_), $args->{ $_ }) } keys(%$args);
  19         67  
178 5         41 });
179              
180 5         37 return $collection;
181             }
182              
183             =head2 filter
184              
185             Filter objects by callback
186              
187             Returns:
188             C of C objects
189              
190             =cut
191              
192             sub filter {
193 29     29 1 198295 my ($self, $callback) = @_;
194              
195 29         101 my @objects = grep { $callback->($_) } @{ $self->objects };
  115         462  
  29         102  
196              
197 29         165 return $self->new({ objects => \@objects })->as($self->model);
198             }
199              
200             =head2 find
201              
202             Find object by primary key, of if args is a hash ref then find first object matching given args
203              
204             Returns:
205             C object
206              
207             =cut
208              
209             sub find {
210 8     8 1 219229 my ($self, $args) = @_;
211              
212 8 100       35 if (ref($args) eq 'HASH') {
213             return $self->__first(sub {
214 6     6   10 my $object = shift;
215              
216 6         26 return List::Util::all { $self->comparator->verify($object->get($_), $args->{ $_ }) } keys(%$args);
  6         16  
217 3         22 });
218             }
219              
220             my $object = $self->__first(sub {
221 11     11   19 my $object = shift;
222              
223 11         40 return ($object->pk eq $args);
224 5         38 });
225              
226 5         49 return $object;
227             }
228              
229             =head2 findOrNew
230              
231             Find object or create a new instance
232              
233             Returns:
234             C object
235              
236             =cut
237              
238             sub findOrNew {
239 2     2 1 165191 my ($self, $args, $extra) = @_;
240              
241 2 50       5 my %new_args = (%{ $args || {} }, %{ $extra || {} });
  2 50       9  
  2         15  
242              
243 2   66     11 return $self->find($args) || $self->newObject(%new_args);
244             }
245              
246             =head2 first
247              
248             Get first object
249              
250             Returns:
251             C object
252              
253             =cut
254              
255             sub first {
256 8     8 1 241434 return shift->get(0);
257             }
258              
259             =head2 firstOrNew
260              
261             Get first object if exists, otherwise create one
262              
263             Returns:
264             C object
265              
266             =cut
267              
268             sub firstOrNew {
269 2     2 1 189177 my ($self, $args) = @_;
270              
271 2   66     9 return $self->first || $self->newObject(%$args);
272             }
273              
274             =head2 get
275              
276             Get object by index
277              
278             Returns:
279             C of C objects
280              
281             =cut
282              
283             sub get {
284 29     29 1 250415 my ($self, $index) = @_;
285              
286 29         135 return $self->objects->[$index];
287             }
288              
289             =head2 indexOf
290              
291             Get the index of an object
292              
293             Returns:
294             Integer index
295              
296             =cut
297              
298             sub indexOf {
299 4     4 1 236671 my ($self, $object) = @_;
300              
301 4         25 my $index = -1;
302 4         8 my @objects = @{ $self->objects };
  4         15  
303              
304 4 100       35 if (ref($object) ne ref($self->model)) {
305 1         12 $object = $self->newObject(%$object);
306             }
307              
308 4         36 for (my $i = 0; $i < scalar(@objects); $i++) {
309 6 100       32 if ($objects[$i]->pk eq $object->pk) {
310 4         23 $index = $i;
311 4         11 last;
312             }
313             }
314              
315 4         27 return $index;
316             }
317              
318             =head2 intersect
319              
320             Return a new collection containing only items that exist in both collections.
321             Uses $key to compare objects (defaults to primary_key).
322              
323             Returns:
324             C of C objects
325              
326             =cut
327              
328             sub intersect {
329 0     0 1 0 my ($self, $other_collection, $key) = @_;
330              
331 0   0     0 $key ||= $self->model->primary_key;
332              
333 0         0 return $self->search({
334             $key => $other_collection->lists($key),
335             });
336             }
337              
338             =head2 last
339              
340             Get last object
341              
342             Returns:
343             C object
344              
345             =cut
346              
347             sub last {
348 7     7 1 193048 my $self= shift;
349              
350 7         10 return $self->get(scalar(@{ $self->objects }) - 1);
  7         26  
351             }
352              
353             =head2 lists
354              
355             Return an array ref containing all the fields from all objects for the given $field.
356             Return a hash ref containing $key_field => $value_field if both are given.
357              
358             =cut
359              
360             sub lists {
361 7     7 1 165486 my ($self, $key_field, $value_field) = @_;
362              
363 7 50 33     51 if ($key_field && $value_field) {
364 0         0 my %list = map { $_->get($key_field) => $_->get($value_field) } @{ $self->objects };
  0         0  
  0         0  
365              
366 0         0 return \%list;
367             }
368              
369 7         17 my @fields = map { $self->__list($_->get($key_field)) } @{ $self->objects };
  27         159  
  7         31  
370              
371 7         51 return \@fields;
372             }
373              
374             =head2 max
375              
376             Return the maximum value for given $field
377              
378             =cut
379              
380             sub max {
381 0     0 1 0 my ($self, $field) = @_;
382 0         0 my $values = $self->lists($field);
383              
384 0         0 return List::Util::max(@$values);
385             }
386              
387             =head2 min
388              
389             Return the minimum value for given $field
390              
391             =cut
392              
393             sub min {
394 0     0 1 0 my ($self, $field) = @_;
395 0         0 my $values = $self->lists($field);
396              
397 0         0 return List::Util::min(@$values);
398             }
399              
400             =head2 missing
401              
402             Return a new collection containing only items that exist in this collection
403             but not in the other collection.
404             Uses $key to compare objects (defaults to primary_key).
405              
406             Returns:
407             C of C objects
408              
409             =cut
410              
411             sub missing {
412 0     0 1 0 my ($self, $other_collection, $key) = @_;
413              
414 0   0     0 $key ||= $self->model->primary_key;
415              
416 0         0 return $self->exclude({
417             $key => $other_collection->lists($key),
418             });
419             }
420              
421             =head2 newObject
422              
423             Instantiate new object
424              
425             Returns:
426             C object
427              
428             =cut
429              
430             sub newObject {
431 139     139 1 275 my $self = shift;
432              
433 139 100       353 if (ref($self->model) eq 'CODE') {
434 6         25 return $self->model->(@_);
435             }
436              
437 133         743 return $self->model->new(@_);
438             }
439              
440             =head2 next
441              
442             Get next object
443              
444             Returns:
445             C object
446              
447             =cut
448              
449             sub next {
450 5     5 1 255483 my $self = shift;
451 5         24 my $index = $self->index;
452              
453 5         56 $self->index($index + 1);
454              
455 5         48 my $object = $self->get($index);
456              
457             # Auto reset
458 5 100       43 if (! $object) {
459 1         7 $self->reset;
460             }
461              
462 5         19 return $object;
463             }
464              
465             =head2 only
466              
467             Return an array ref containing only given @keys
468              
469             =cut
470              
471             sub only {
472 10     10 1 178586 my ($self, @keys) = @_;
473              
474 10         48 my $only = [];
475              
476 10         38 foreach my $object (@{ $self->objects }) {
  10         33  
477 28         102 push @$only, { map { $_ => $object->get($_) } @keys };
  52         151  
478             }
479              
480 10         131 return $only;
481             }
482              
483             =head2 orderBy
484              
485             Order collection
486              
487             =cut
488              
489             sub orderBy {
490 2     2 1 228881 my ($self, $field, $direction, $string) = @_;
491              
492 2   100     15 $direction ||= 'asc';
493 2 50       7 $string = 1 unless defined $string;
494              
495 2         5 my @objects = @{ $self->objects };
  2         11  
496 2         12 my @sorted;
497              
498 2 100       8 if ($direction eq 'asc') {
499 1 50       4 if ($string) {
500 1         8 @sorted = sort { $a->get($field, '') cmp $b->get($field, '') } @objects;
  4         20  
501             }
502             else {
503 0         0 @sorted = sort { $a->get($field, '') <=> $b->get($field, '') } @objects;
  0         0  
504             }
505             } else {
506 1 50       5 if ($string) {
507 1         5 @sorted = sort { $b->get($field, '') cmp $a->get($field, '')} @objects;
  4         15  
508             }
509             else {
510 0         0 @sorted = sort { $b->get($field, '') <=> $a->get($field, '')} @objects;
  0         0  
511             }
512             }
513              
514 2         11 $self->objects(\@sorted);
515              
516 2         24 return $self;
517             }
518              
519             =head2 page
520              
521             Take a collection containing only the results from a given page
522              
523             Returns:
524             C of C objects
525              
526             =cut
527              
528             sub page {
529 3     3 1 240082 my ($self, $page) = @_;
530 3   50     14 $page ||= 1;
531              
532 3         8 my @objects = @{ $self->objects };
  3         13  
533              
534 3         25 my $start = ($page - 1) * $self->limit;
535 3         21 my $end = List::Util::min(($page * $self->limit), scalar(@objects)) - 1;
536              
537 3         23 my $last_page = int((scalar(@objects) + $self->limit - 1) / $self->limit);
538              
539 3 100       38 my $pager = {
    100          
540             count => $self->count,
541             limit => $self->limit,
542             prev_page => ($page > 1) ? $page - 1 : 1,
543             first_page => 1,
544             page => $page,
545             next_page => ($page < $last_page) ? $page + 1 : $last_page,
546             last_page => $last_page,
547             start => $start + 1,
548             end => $end + 1,
549             };
550              
551 3         91 my $next = $self->slice($start, $end);
552              
553 3         16 $next->pager($pager);
554              
555 3         37 return $next;
556             }
557              
558             =head2 remove
559              
560             Remove an object
561              
562             =cut
563              
564             sub remove {
565 1     1 1 3 my ($self, $object) = @_;
566              
567 1 50       4 if (ref($object) eq 'ARRAY') {
568 0         0 $self->remove($_) for (@{ $object });
  0         0  
569              
570 0         0 return $self;
571             }
572              
573 1         17 my @objects = @{ $self->objects };
  1         4  
574              
575             # Remove only if exists
576 1 50       9 if ($self->indexOf($object) >= 0) {
577 1         3 return $self->splice($self->indexOf($object), 1);
578             }
579              
580 0         0 return $self;
581             }
582              
583             =head2 reset
584              
585             Reset index
586              
587             =cut
588              
589             sub reset {
590 1     1 1 3 my $self = shift;
591              
592 1         5 $self->index(0);
593              
594 1         8 return $self;
595             }
596              
597             =head2 search
598              
599             Search objects by given args
600              
601             Returns:
602             C of C objects
603              
604             =cut
605              
606             sub search {
607 22     22 1 22001 my ($self, $args) = @_;
608              
609             my $collection = $self->filter(sub {
610 88     88   179 my $object = shift;
611              
612 88         526 return List::Util::all { $self->comparator->verify($object->get($_), $args->{ $_ }) } keys(%$args);
  88         298  
613 22         219 });
614              
615 22         203 return $collection;
616             }
617              
618             =head2 slice
619              
620             Take a slice from the collection
621              
622             Returns:
623             C of C objects
624              
625             =cut
626              
627             sub slice {
628 4     4 1 237214 my ($self, $start, $end) = @_;
629              
630 4   50     15 $start //= 0;
631 4   33     13 $end //= $self->count - $start;
632              
633 4         7 my @objects = @{ $self->objects };
  4         15  
634 4         30 my @partition = @objects[$start .. $end];
635              
636 4         20 return $self->new({ objects => \@partition })->as($self->model);
637             }
638              
639             =head2 splice
640              
641             Splice objects
642              
643             =cut
644              
645             sub splice {
646 4     4 1 206805 my ($self, $offset, $length) = @_;
647              
648 4 100       15 if (! $length) {
649 2   100     9 $length = $offset || 1;
650 2         3 $offset = 0;
651             }
652              
653 4         6 my @objects = @{ $self->objects };
  4         14  
654              
655 4         34 splice(@objects, $offset, $length);
656              
657 4         13 $self->objects(\@objects);
658              
659 4         42 return $self;
660             }
661              
662             =head2 sum
663              
664             Sum by $field
665              
666             =cut
667              
668             sub sum {
669 1     1 1 146922 my ($self, $field) = @_;
670 1         6 my $values = $self->lists($field);
671              
672 1         17 return List::Util::sum(@$values);
673             }
674              
675             =head2 toArray
676              
677             Convert collection to array ref
678              
679             =cut
680              
681             sub toArray {
682 2     2 1 148832 my $self = shift;
683              
684 2         7 return $self->formatter->toArray($self->objects, @_);
685             }
686              
687             =head2 toCsv
688              
689             Convert collection to CSV string
690              
691             =cut
692              
693             sub toCsv {
694 1     1 1 264887 my $self = shift;
695              
696 1         6 return $self->formatter->toCsv($self->objects, @_);
697             }
698              
699             =head2 toJson
700              
701             Convert collection to JSON string
702              
703             =cut
704              
705             sub toJson {
706 2     2 1 267065 my $self = shift;
707              
708 2         12 return $self->formatter->toJson($self->objects, @_);
709             }
710              
711             =head2 touch
712              
713             Touch collections model
714              
715             =cut
716              
717             sub touch {
718 0     0 1 0 my ($self, $callback) = @_;
719              
720 0         0 my $objects = $self->objects;
721              
722 0         0 for (my $i = 0; $i < $self->count; $i++) {
723 0         0 $callback->($objects->[$i]);
724             }
725              
726 0         0 $self->objects($objects);
727              
728 0         0 return $self;
729             }
730              
731             =head2 unique
732              
733             Return an array ref containing only unique values for given $field
734              
735             =cut
736              
737             sub unique {
738 1     1 1 156490 my ($self, $field) = @_;
739              
740 1         2 my %fields = map { $_->get($field) => 1 } @{ $self->objects };
  4         11  
  1         3  
741              
742 1         4 my @unique = keys(%fields);
743              
744 1         7 return \@unique;
745             }
746              
747             =head2 where
748              
749             Search objects where field is equal to value
750              
751             Returns:
752             C of C objects
753              
754             =cut
755              
756             sub where {
757 0     0 1 0 my $self = shift;
758              
759 0         0 return $self->search(@_);
760             }
761              
762             =head2 whereIn
763              
764             Search objects where field is in $array
765              
766             Returns:
767             C of C objects
768              
769             =cut
770              
771             sub whereIn {
772 1     1 1 254908 my ($self, $field, $array) = @_;
773              
774             my $collection = $self->filter(sub {
775 4     4   20 my $object = shift;
776              
777 4         10 return grep { $object->get($field) eq $_ } @$array;
  8         37  
778 1         12 });
779              
780 1         12 return $collection;
781             }
782              
783             =head2 whereNotIn
784              
785             Search objects where field is not in $array
786              
787             Returns:
788             C of C objects
789              
790             =cut
791              
792             sub whereNotIn {
793 1     1 1 268274 my ($self, $field, $array) = @_;
794              
795 1         8 return $self->exclude({ $field => $array });
796             }
797              
798             =head2 __first
799              
800             Find first object that match $callback
801              
802             Returns:
803             C object
804              
805             =cut
806              
807             sub __first {
808 8     8   20 my ($self, $callback) = @_;
809              
810 8     17   30 return List::Util::first { $callback->($_) } @{ $self->objects };
  17         118  
  8         31  
811             }
812              
813             =head2 __list
814              
815             Return a scalar, or an array if the value is a collection
816              
817             =cut
818              
819             sub __list {
820 27     27   58 my ($self, $value) = @_;
821              
822 27 50 33     83 if (blessed($value) && $value->isa('Mojo::Util::Collection')) {
823 0         0 return @{ $value->objects };
  0         0  
824             }
825              
826 27         72 return $value;
827             }
828              
829             1;