File Coverage

lib/File/DataClass/ResultSet.pm
Criterion Covered Total %
statement 126 126 100.0
branch 21 28 75.0
condition 9 13 69.2
subroutine 49 49 100.0
pod 16 16 100.0
total 221 232 95.2


line stmt bran cond sub pod time code
1             package File::DataClass::ResultSet;
2              
3 3     3   13 use namespace::autoclean;
  3         4  
  3         14  
4              
5 3     3   155 use File::DataClass::Constants qw( EXCEPTION_CLASS FALSE TRUE );
  3         5  
  3         205  
6 3     3   12 use File::DataClass::Functions qw( is_arrayref is_hashref is_member throw );
  3         5  
  3         150  
7 3     3   1029 use File::DataClass::List;
  3         6  
  3         79  
8 3     3   1101 use File::DataClass::Result;
  3         6  
  3         75  
9 3         10 use File::DataClass::Types qw( ArrayRef ClassName
10 3     3   14 HashRef Int Maybe Object Str );
  3         5  
11 3     3   2239 use Scalar::Util qw( blessed );
  3         3  
  3         255  
12 3     3   1415 use Subclass::Of;
  3         22626  
  3         82  
13 3     3   487 use Unexpected::Functions qw( RecordNotFound Unspecified );
  3         4  
  3         27  
14 3     3   761 use Moo;
  3         5  
  3         17  
15              
16             my $class_stash = {};
17              
18             # Private functions
19             my $_build_operators = sub {
20             return {
21 3     3   23 'eq' => sub { return $_[ 0 ] eq $_[ 1 ] },
22 5     5   34 '==' => sub { return $_[ 0 ] == $_[ 1 ] },
23 3     3   18 'ne' => sub { return $_[ 0 ] ne $_[ 1 ] },
24 3     3   19 '!=' => sub { return $_[ 0 ] != $_[ 1 ] },
25 5     5   29 '>' => sub { return $_[ 0 ] > $_[ 1 ] },
26 3     3   20 '>=' => sub { return $_[ 0 ] >= $_[ 1 ] },
27 3     3   18 '<' => sub { return $_[ 0 ] < $_[ 1 ] },
28 3     3   17 '<=' => sub { return $_[ 0 ] <= $_[ 1 ] },
29 3     3   4 '=~' => sub { my $re = $_[ 1 ]; return $_[ 0 ] =~ qr{ $re }mx },
  3         38  
30 3     3   3 '!~' => sub { my $re = $_[ 1 ]; return $_[ 0 ] !~ qr{ $re }mx },
  3         33  
31 12     12   336 };
32             };
33              
34             # Public attributes
35             has 'list_class' => is => 'ro', isa => ClassName,
36             default => 'File::DataClass::List';
37              
38             has 'result_class' => is => 'ro', isa => ClassName,
39             default => 'File::DataClass::Result';
40              
41             has 'result_source' => is => 'ro', isa => Object,
42             handles => [ qw( attributes defaults label_attr path storage ) ],
43             required => TRUE, weak_ref => TRUE;
44              
45             has '_iterator' => is => 'rw', isa => Int, default => 0,
46             init_arg => undef;
47              
48             has '_operators' => is => 'lazy', isa => HashRef,
49             builder => $_build_operators;
50              
51             has '_results' => is => 'rw', isa => ArrayRef,
52 20     20   8145 builder => sub { [] }, init_arg => undef;
53              
54             # Private methods
55             my $_get_attr_meta = sub {
56             my ($types, $source, $values, $attr) = @_;
57              
58             my $sdef = $source->defaults->{ $attr };
59             my $type = $source->types->{ $attr }
60             // $types->{ ref $sdef || ref $values->{ $attr } || 'SCALAR' };
61              
62             return [ is => 'rw', isa => $type ];
63             };
64              
65             my $_new_result_class = sub {
66             my ($class, $source, $values) = @_;
67              
68             my $name = "${class}::".(ucfirst $source->name);
69              
70             exists $class_stash->{ $name } and return $class_stash->{ $name };
71              
72             my $except = 'delete | id | insert | name | result_source | update';
73             my %types = ( 'ARRAY', Maybe[ArrayRef],
74             'HASH', Maybe[HashRef],
75             'SCALAR', Maybe[Str], );
76             my @attrs = map { $_ => $_get_attr_meta->( \%types, $source, $values, $_ )}
77             grep { not m{ \A (?: $except ) \z }mx }
78             @{ $source->attributes };
79              
80             return $class_stash->{ $name } = subclass_of
81             ( $class, -package => $name, -has => [ @attrs ] );
82             };
83              
84             my $_create_result = sub {
85             my ($self, $args) = @_;
86              
87             my $attr = { %{ $self->defaults }, result_source => $self->result_source };
88              
89             for (grep { exists $args->{ $_ } and defined $args->{ $_ } }
90             @{ $self->attributes }, 'id', 'name') {
91             $attr->{ $_ } = $args->{ $_ };
92             }
93              
94             my $class = $_new_result_class->
95             ( $self->result_class, $self->result_source, $attr );
96              
97             return $class->new( $attr );
98             };
99              
100             my $_eval_op = sub {
101             my ($self, $lhs, $op, $rhs) = @_;
102              
103             my $subr = $self->_operators->{ $op } or return FALSE;
104              
105             $_ or return FALSE for (map { $subr->( $_, $rhs ) ? 1 : 0 }
106             (is_arrayref $lhs) ? @{ $lhs } : ( $lhs ));
107              
108             return TRUE;
109             };
110              
111             my $_push = sub {
112             my ($self, $id, $attr, $items) = @_;
113              
114             my $attrs = { %{ $self->select->{ $id } // {} }, id => $id };
115             my $list = [ @{ $attrs->{ $attr } // [] } ];
116             my $in = [];
117              
118             for my $item (grep { not is_member $_, $list } @{ $items }) {
119             CORE::push @{ $list }, $item; CORE::push @{ $in }, $item;
120             }
121              
122             $attrs->{ $attr } = $list;
123             return ($attrs, $in);
124             };
125              
126             my $_splice = sub {
127             my ($self, $id, $attr, $items) = @_;
128              
129             my $attrs = { %{ $self->select->{ $id } // {} }, id => $id };
130             my $list = [ @{ $attrs->{ $attr } // [] } ];
131             my $out = [];
132              
133             for my $item (@{ $items }) {
134             defined $list->[ 0 ] or last;
135              
136             for (0 .. $#{ $list }) {
137             if ($list->[ $_ ] eq $item) {
138             CORE::splice @{ $list }, $_, 1; CORE::push @{ $out }, $item;
139             last;
140             }
141             }
142             }
143              
144             $attrs->{ $attr } = $list;
145             return ($attrs, $out);
146             };
147              
148             my $_txn_do = sub {
149             my ($self, $coderef) = @_;
150              
151             return $self->storage->txn_do( $self->path, $coderef );
152             };
153              
154             my $_update_result = sub {
155             my ($self, $result, $args) = @_;
156              
157             for my $attr (grep { exists $args->{ $_ } } @{ $self->attributes }) {
158             $result->$attr( $args->{ $attr } );
159             }
160              
161             return $result->update;
162             };
163              
164             my $_validate_params = sub {
165             my ($self, $args) = @_; $args //= {};
166              
167             my $id = (is_hashref $args) ? ($args->{id} // $args->{name}) : $args;
168              
169             $id or throw Unspecified, [ 'record id' ], level => 2;
170              
171             return $id;
172             };
173              
174             my $_eval_clause = sub {
175             my ($self, $clause, $lhs) = @_;
176              
177             if (is_hashref $clause) {
178             for (keys %{ $clause }) {
179             $self->$_eval_op( $lhs, $_, $clause->{ $_ } ) or return FALSE;
180             }
181              
182             return TRUE;
183             }
184             elsif (is_arrayref $clause) { # TODO: Handle case of 2 arrays
185             return (is_arrayref $lhs) ? FALSE : (is_member $lhs, $clause);
186             }
187              
188             return (is_arrayref $lhs) ? ((is_member $clause, $lhs) ? TRUE : FALSE)
189             : ($clause eq $lhs ? TRUE : FALSE);
190             };
191              
192             my $_find = sub {
193             my ($self, $id) = @_; my $results = $self->select;
194              
195             ($id and exists $results->{ $id }) or return;
196              
197             my $attrs = { %{ $results->{ $id } }, id => $id };
198              
199             return $self->$_create_result( $attrs );
200             };
201              
202             my $_list = sub {
203             my ($self, $id) = @_; my ($attr, $attrs, $labels); my $found = FALSE;
204              
205             my $results = $self->select; my $list = [ sort keys %{ $results } ];
206              
207             $attr = $self->label_attr
208             and $labels = { map { $_ => $results->{ $_ }->{ $attr } } @{ $list } };
209              
210             if ($id and exists $results->{ $id }) {
211             $attrs = { %{ $results->{ $id } }, id => $id }; $found = TRUE;
212             }
213             else { $attrs = { id => $id } }
214              
215             my $result = $self->$_create_result( $attrs );
216              
217             $attrs = { found => $found, list => $list, result => $result, };
218             $labels and $attrs->{labels} = $labels;
219             return $self->list_class->new( $attrs );
220             };
221              
222             my $_eval_criteria = sub {
223             my ($self, $criteria, $attrs) = @_; my $lhs;
224              
225             for my $k (keys %{ $criteria }) {
226             defined ($lhs = $attrs->{ $k eq 'name' ? 'id' : $k }) or return FALSE;
227             $self->$_eval_clause( $criteria->{ $k }, $lhs ) or return FALSE;
228             }
229              
230             return TRUE;
231             };
232              
233             my $_find_and_update = sub {
234             my ($self, $args) = @_; my $id = $self->$_validate_params( $args );
235              
236             my $result = $self->$_find( $id )
237             or throw RecordNotFound, [ $self->path, $id ];
238              
239             return $self->$_update_result( $result, $args );
240             };
241              
242             my $_search = sub {
243             my ($self, $where) = @_; my $results = $self->_results; my @tmp;
244              
245             if (not defined $results->[ 0 ]) {
246             $results = $self->select;
247              
248             for (keys %{ $results }) {
249             my $attrs = { %{ $results->{ $_ } }, id => $_ };
250              
251             if (not $where or $self->$_eval_criteria( $where, $attrs )) {
252             CORE::push @{ $self->_results }, $self->$_create_result( $attrs );
253             }
254             }
255             }
256             elsif ($where and defined $results->[ 0 ]) {
257             for (@{ $results }) {
258             $self->$_eval_criteria( $where, $_ ) and CORE::push @tmp, $_;
259             }
260              
261             $self->_results( \@tmp );
262             }
263              
264             return wantarray ? $self->all : $self;
265             };
266              
267             # Public methods
268             sub all {
269 12     12 1 18 my $self = shift; return @{ $self->_results };
  12         8  
  12         154  
270             }
271              
272             sub create {
273 6     6 1 4416 my ($self, $args) = @_; $self->$_validate_params( $args );
  6         17  
274              
275 4     4   26 return $self->$_txn_do( sub { $self->$_create_result( $args )->insert } );
  4         13  
276             }
277              
278             sub create_or_update {
279 5     5 1 1970 my ($self, $args) = @_; my $id = $self->$_validate_params( $args );
  5         18  
280              
281             return $self->$_txn_do( sub {
282 5 100   5   12 my $result = $self->$_find( $id )
283             or return $self->$_create_result( $args )->insert;
284              
285 3         195 return $self->$_update_result( $result, $args );
286 5         30 } );
287             }
288              
289             sub delete {
290 8     8 1 2880 my ($self, $args) = @_; my $id = $self->$_validate_params( $args );
  8         21  
291              
292 8         152 my $path = $self->path;
293 8 50       469 my $optional = (is_hashref $args) ? $args->{optional} : FALSE;
294             my $res = $self->$_txn_do( sub {
295 8 100   8   10 my $result; unless ($result = $self->$_find( $id )) {
  8         19  
296 3 100       15 $optional or throw RecordNotFound, [ $path, $id ];
297 1         3 return FALSE;
298             }
299              
300             $result->delete
301 5 50       302 or throw 'File [_1] source [_2] not deleted', [ $path, $id ];
302 5         22 return TRUE;
303 8         43 } );
304              
305 6 100       66 return $res ? $id : undef;
306             }
307              
308             sub find {
309 6     6 1 2070 my ($self, $args) = @_; my $id = $self->$_validate_params( $args );
  6         16  
310              
311 6     6   36 return $self->$_txn_do( sub { $self->$_find( $id ) } );
  6         14  
312             }
313              
314             sub find_and_update {
315 1     1 1 87 my ($self, $args) = @_; $self->$_validate_params( $args );
  1         2  
316              
317 1     1   4 return $self->$_txn_do( sub { $self->$_find_and_update( $args ) } );
  1         3  
318             }
319              
320             sub first {
321 1     1 1 2 my $self = shift; return $self->_results->[ 0 ];
  1         12  
322             }
323              
324             sub last {
325 1     1 1 2 my $self = shift; return $self->_results->[ -1 ];
  1         14  
326             }
327              
328             sub list {
329 2     2 1 846 my ($self, $args) = @_;
330              
331 2 50 33     9 my $id = (is_hashref $args) ? $args->{id} // $args->{name} : $args;
332              
333 2     2   11 return $self->$_txn_do( sub { $self->$_list( $id ) } );
  2         5  
334             }
335              
336             sub next {
337 3     3 1 16 my $self = shift;
338 3         39 my $index = $self->_iterator; $self->_iterator( $index + 1 );
  3         43  
339              
340 3         77 return $self->_results->[ $index ];
341             }
342              
343             sub push {
344 2     2 1 2212 my ($self, $args) = @_; my $id = $self->$_validate_params( $args );
  2         7  
345              
346 2 50       6 my $list = $args->{list} or throw Unspecified, [ 'list' ];
347 2   100     7 my $items = $args->{items} // []; my ($added, $attrs);
  2         3  
348              
349 2 100       7 $items->[ 0 ] or throw 'List contains no items';
350              
351             my $res = $self->$_txn_do( sub {
352 1     1   4 ($attrs, $added) = $self->$_push( $id, $list, $items );
353              
354 1         4 return $self->$_find_and_update( $attrs );
355 1         7 } );
356              
357 1 50       23 return $res ? $added : FALSE;
358             }
359              
360             sub reset {
361 1     1 1 5 my $self = shift; return $self->_iterator( 0 );
  1         12  
362             }
363              
364             sub select {
365 43     43 1 48 my $self = shift;
366              
367 43         597 return $self->storage->select( $self->path, $self->result_source->name );
368             }
369              
370             sub search {
371 17     17 1 2869 my ($self, $args) = @_;
372              
373 17     17   117 return $self->$_txn_do( sub { $self->$_search( $args ) } );
  17         29  
374             }
375              
376             sub splice {
377 2     2 1 2711 my ($self, $args) = @_; my $id = $self->$_validate_params( $args );
  2         5  
378              
379 2 50       6 my $list = $args->{list} or throw Unspecified, [ 'list' ];
380 2   100     9 my $items = $args->{items} // []; my ($attrs, $removed);
  2         2  
381              
382 2 100       8 $items->[ 0 ] or throw 'List contains no items';
383              
384             my $res = $self->$_txn_do( sub {
385 1     1   3 ($attrs, $removed) = $self->$_splice( $id, $list, $items );
386              
387 1         3 return $self->$_find_and_update( $attrs );
388 1         7 } );
389              
390 1 50       9 return $res ? $removed : FALSE;
391             }
392              
393             sub update {
394 4     4 1 1638 my ($self, $args) = @_;
395              
396 4 100 66     24 if (my $id = $args->{id} // $args->{name}) { # Deprecated
397 3     3   19 return $self->$_txn_do( sub { $self->$_find_and_update( $args ) } );
  3         11  
398             }
399              
400             return $self->$_txn_do( sub {
401 1     1   2 my $updated = FALSE;
402              
403 1         2 for my $result (@{ $self->_results }) {
  1         15  
404 2   66     6 my $res = $self->$_update_result( $result, $args ); $updated ||= $res;
  2         9  
405             }
406              
407 1         4 return $updated;
408 1         6 } );
409             }
410              
411             1;
412              
413             __END__