File Coverage

blib/lib/Yancy/Backend/Memory.pm
Criterion Covered Total %
statement 202 205 98.5
branch 90 100 90.0
condition 81 95 85.2
subroutine 20 20 100.0
pod 1 9 11.1
total 394 429 91.8


line stmt bran cond sub pod time code
1             package Yancy::Backend::Memory;
2             our $VERSION = '1.086';
3             # ABSTRACT: A backend entirely in memory
4              
5             #pod =head1 DESCRIPTION
6             #pod
7             #pod An in-memory "database" backend for Yancy. Uses L to implement
8             #pod basic searching for ().
9             #pod
10             #pod =cut
11              
12             # XXX: TODO Remove references to Local::Test
13              
14 23     23   20044 use Mojo::Base '-base';
  23         65  
  23         178  
15 23     23   4256 use List::Util qw( max );
  23         53  
  23         1560  
16 23     23   191 use Mojo::JSON qw( true false from_json to_json encode_json );
  23         54  
  23         1559  
17 23     23   155 use Mojo::File qw( path );
  23         49  
  23         1165  
18 23     23   150 use Storable qw( dclone );
  23         49  
  23         1227  
19 23     23   146 use Role::Tiny qw( with );
  23         66  
  23         286  
20             with 'Yancy::Backend::Role::Sync';
21 23     23   5158 use Yancy::Util qw( match is_type order_by is_format );
  23         52  
  23         1532  
22 23     23   14107 use Time::Piece;
  23         167155  
  23         112  
23              
24             our %DATA;
25              
26             sub new {
27 84     84 1 75596 my ( $class, $url, $schema ) = @_;
28 84 100       300 if ( $url ) {
29 83         522 my ( $path ) = $url =~ m{^[^:]+://[^/]+(?:/(.+))?$};
30 83 50       320 if ( $path ) {
31 0   0     0 %DATA = %{ from_json( path( ( $ENV{MOJO_HOME} || () ), $path )->slurp ) };
  0         0  
32             }
33             }
34 84   100     313 $schema //= \%Local::Test::SCHEMA;
35 84         531 return bless { init_arg => $url, schema => $schema }, $class;
36             }
37              
38             sub schema {
39 3184     3184 0 9600 my ( $self, $schema ) = @_;
40 3184 100       6815 if ( $schema ) {
41 51         144 $self->{schema} = $schema;
42 51         148 return;
43             }
44 3133         10864 $self->{schema};
45             }
46             sub collections;
47             *collections = *schema;
48              
49             sub create {
50 113     113 0 33001 my ( $self, $schema_name, $params ) = @_;
51 113         577 $params = { %$params };
52 113         361 my $props = $self->schema->{ $schema_name }{properties};
53             $params->{ $_ } = $props->{ $_ }{default} // undef
54 113   100     1443 for grep !exists $params->{ $_ },
55             keys %$props;
56 113         513 $params = $self->_normalize( $schema_name, $params ); # makes a copy
57              
58 113   100     399 my $id_field = $self->schema->{ $schema_name }{ 'x-id-field' } || 'id';
59 113 100       433 my @id_fields = ref $id_field eq 'ARRAY' ? @$id_field : ( $id_field );
60              
61             # Fill in any auto-increment data...
62 113         299 for my $id_field ( @id_fields ) {
63             # We haven't provided a value for an integer ID, assume it's autoinc
64 118 100 66     825 if ( !$params->{ $id_field } and $self->schema->{ $schema_name }{properties}{ $id_field }{type} eq 'integer' ) {
    100 66        
      66        
65 55         100 my @existing_ids = keys %{ $DATA{ $schema_name } };
  55         225  
66 55   100     464 $params->{ $id_field} = ( max( @existing_ids ) // 0 ) + 1;
67             }
68             # We have provided another ID, make 'id' another autoinc
69             elsif ( $params->{ $id_field }
70             && $id_field ne 'id'
71             && exists $self->schema->{ $schema_name }{properties}{id}
72             ) {
73 46         94 my @existing_ids = map { $_->{ id } } values %{ $DATA{ $schema_name } };
  45         132  
  46         156  
74 46   100     325 $params->{id} = ( max( @existing_ids ) // 0 ) + 1;
75             }
76             }
77              
78 113   100     404 my $store = $DATA{ $schema_name } //= {};
79 113         476 for my $i ( 0 .. $#id_fields-1 ) {
80 5   100     41 $store = $store->{ $params->{ $id_fields[$i] } } //= {};
81             }
82 113         466 $store->{ $params->{ $id_fields[-1] } } = $params;
83              
84 113 100       738 return @id_fields > 1 ? { map {; $_ => $params->{ $_ } } @id_fields } : $params->{ $id_field };
  10         59  
85             }
86              
87             sub get {
88 327     327 0 475993 my ( $self, $schema_name, $id, %opt ) = @_;
89 327         1000 my $schema = $self->schema->{ $schema_name };
90 327   100     2411 my $real_coll = ( $schema->{'x-view'} || {} )->{schema} // $schema_name;
      66        
91              
92 327   100     905 my $id_field = $self->schema->{ $schema_name }{ 'x-id-field' } || 'id';
93 327 100       1287 my @ids = ref $id_field eq 'ARRAY' ? map { $id->{ $_ } } @$id_field : ( $id );
  25         90  
94 326 100 66     1101 die "Missing composite ID parts" if @ids > 1 && ( !ref $id || keys %$id < @ids );
      100        
95              
96 325         868 my $item = $DATA{ $real_coll };
97 325         765 for my $id ( @ids ) {
98 336 100       996 return undef if !defined $id;
99 333   100     1461 $item = $item->{ $id } // return undef;
100             }
101              
102 280         955 $item = $self->_viewise( $schema_name, $item );
103 280 100       1285 if ( my $join = $opt{join} ) {
104 3         14 $item = $self->_join( $schema_name, $item, $join );
105             }
106              
107 280         1470 return $item;
108             }
109              
110             sub _join {
111 19     19   57 my ( $self, $schema_name, $item, $join, $where ) = @_;
112 19         116 $item = { %$item };
113 19         52 my $schema = $self->schema->{ $schema_name };
114 19   50     44 my $id_field = $self->schema->{ $schema_name }{ 'x-id-field' } || 'id';
115 19 100       75 my @joins = ref $join eq 'ARRAY' ? @$join : ( $join );
116 19         42 for my $join ( @joins ) {
117 26 100       86 if ( my $join_prop = $schema->{ properties }{ $join } ) {
    50          
118 21   100     60 my $join_id = $item->{ $join } || next;
119 17         32 my $join_schema_name = $join_prop->{'x-foreign-key'};
120 17         54 $item->{ $join } = $self->get( $join_schema_name, $join_id );
121 17         157 for my $key ( grep /^${join}\./, keys %$where ) {
122 7         73 my ( $k ) = $key =~ /^${join}\.(.+)$/;
123 7 100       36 if ( !match( { $k => $where->{ $key } }, $item->{ $join } ) ) {
124             # Inner match fails, so this row is not in the
125             # results
126 2         15 return;
127             }
128             }
129             }
130             elsif ( my $join_schema = $self->schema->{ $join } ) {
131 5         11 my $join_schema_name = $join;
132 5   100     9 my ( $join_id_field ) = grep { ( $join_schema->{properties}{$_}{'x-foreign-key'}//'' ) eq $schema_name } keys %{ $join_schema->{properties} };
  20         86  
  5         21  
133 5 50       26 my $join_where = ref $id_field eq 'ARRAY' ? { map { $_ => $item->{ $_ } } @$join_id_field } : { $join_id_field => $item->{$join_id_field} };
  0         0  
134 5         10 my $min_items = 0;
135 5         37 for my $key ( grep /^${join}\./, keys %$where ) {
136 2         22 my ( $k ) = $key =~ /^${join}\.(.+)$/;
137 2         6 $join_where->{ $k } = $where->{ $key };
138 2         6 $min_items = 1;
139             }
140 5         25 my $res = $self->list( $join_schema_name, $join_where );
141 5 100       23 return if $res->{total} < $min_items;
142 4         18 $item->{ $join } = $res->{items};
143             }
144             }
145 16         61 return $item;
146             }
147              
148             sub _viewise {
149 626     626   1975 my ( $self, $schema_name, $item, $join ) = @_;
150 626         23186 $item = dclone $item;
151 626         2281 my $schema = $self->schema->{ $schema_name };
152 626   100     3797 my $real_coll = ( $schema->{'x-view'} || {} )->{schema} // $schema_name;
      66        
153             my %props = %{
154 626         1250 $schema->{properties} || $self->schema->{ $real_coll }{properties}
155 626 100       4567 };
156 626 100       1866 if ( $join ) {
157 12 100       18 $props{ $_ } = 1 for @{ ref $join eq 'ARRAY' ? $join : [ $join ] };
  12         57  
158             }
159 626         4130 delete $item->{$_} for grep !$props{ $_ }, keys %$item;
160 626         2781 $item;
161             }
162              
163             sub list {
164 327     327 0 394652 my ( $self, $schema_name, $params, @opt ) = @_;
165 327 100       1246 my $opt = @opt % 2 == 0 ? {@opt} : $opt[0];
166 327         983 my $schema = $self->schema->{ $schema_name };
167 327 50       982 die "list attempted on non-existent schema '$schema_name'" unless $schema;
168 327   100     1210 $params ||= {};
169              
170 327   100     712 my $id_field = $self->schema->{ $schema_name }{ 'x-id-field' } || 'id';
171 327 100       1145 my @id_fields = ref $id_field eq 'ARRAY' ? @$id_field : ( $id_field );
172              
173 327   100     2158 my $real_coll = ( $schema->{'x-view'} || {} )->{schema} // $schema_name;
      66        
174             my $props = $schema->{properties}
175 327   33     1126 || $self->schema->{ $real_coll }{properties};
176 327         558 my @rows = values %{ $DATA{ $real_coll } };
  327         1179  
177 327         1138 for my $id_field ( 1..$#id_fields ) {
178 8         41 @rows = map values %$_, @rows;
179             }
180 327 100       979 if ( $opt->{join} ) {
181 6         54 @rows = map $self->_join( $schema_name, $_, $opt->{join}, $params ), @rows;
182             }
183             # Join queries have been resolved
184 327 100       1081 for my $p ( ref $params eq 'ARRAY' ? @$params : ( $params ) ) {
185 328         1365 for my $key ( grep /\./, keys %$p ) {
186 4         9 delete $p->{ $key };
187 4         13 my ( $j ) = split /\./, $key;
188 4         15 $p->{ $j } = { '!=' => undef };
189             }
190             }
191             my $matched_rows = order_by(
192             $opt->{order_by} // \@id_fields,
193 327   100     1535 [ grep { match( $params, $_ ) } @rows ],
  537         1533  
194             );
195 327   100     1373 my $first = $opt->{offset} // 0;
196 327 100       964 my $last = $opt->{limit} ? $opt->{limit} + $first - 1 : $#$matched_rows;
197 327 100       944 if ( $last > $#$matched_rows ) {
198 57         118 $last = $#$matched_rows;
199             }
200 327         1635 my @items = map $self->_viewise( $schema_name, $_, $opt->{join} ), @$matched_rows[ $first .. $last ];
201 327         1271 my $retval = {
202             items => \@items,
203             total => scalar @$matched_rows,
204             };
205             #; use Data::Dumper;
206             #; say Dumper $retval;
207 327         1846 return $retval;
208             }
209              
210             sub set {
211 58     58 0 50573 my ( $self, $schema_name, $id, $params ) = @_;
212 58   100     199 my $id_field = $self->schema->{ $schema_name }{ 'x-id-field' } || 'id';
213 58 100       275 my @id_fields = ref $id_field eq 'ARRAY' ? @$id_field : ( $id_field );
214 58 100 100     235 die "Missing composite ID parts" if @id_fields > 1 && ( !ref $id || keys %$id < @id_fields );
      100        
215              
216             # Fill in any missing params from the ID
217 56         157 for my $id_field ( @id_fields ) {
218 58 100       202 my $id_part = ref $id eq 'HASH' ? $id->{ $id_field } : $id;
219 58 100       602 if ( !$params->{ $id_field } ) {
220 34         151 $params->{ $id_field } = $id_part;
221             }
222             }
223              
224 56         209 $params = $self->_normalize( $schema_name, $params );
225              
226 56         170 my $store = $DATA{ $schema_name };
227 56         224 for my $i ( 0..$#id_fields-1 ) {
228 2         7 my $id_field = $id_fields[ $i ];
229 2 50       16 my $id_part = ref $id eq 'HASH' ? $id->{ $id_field } : $id;
230 2 50       10 return 0 if !$store->{ $id_part };
231             # Update the item's ID if it changes
232 2         7 my $item = delete $store->{ $id_part };
233 2         9 $store->{ $params->{ $id_field } } = $item;
234 2         6 $store = $item;
235             }
236 56 100       200 my $id_part = ref $id eq 'HASH' ? $id->{ $id_fields[-1] } : $id;
237 56 100       225 return 0 if !$store->{ $id_part };
238             $store->{ $params->{ $id_fields[-1] } } = {
239 46         97 %{ delete $store->{ $id_part } },
  46         596  
240             %$params,
241             };
242              
243 46         294 return 1;
244             }
245              
246             sub delete {
247 94     94 0 838294 my ( $self, $schema_name, $id ) = @_;
248 94 50       341 return 0 if !$id;
249 94   100     267 my $id_field = $self->schema->{ $schema_name }{ 'x-id-field' } || 'id';
250 94 100       365 my @id_fields = ref $id_field eq 'ARRAY' ? @$id_field : ( $id_field );
251 94 100 100     315 die "Missing composite ID parts" if @id_fields > 1 && ( !ref $id || keys %$id < @id_fields );
      100        
252 92         208 my $store = $DATA{ $schema_name };
253 92         285 for my $i ( 0..$#id_fields-1 ) {
254 2         9 my $id_field = $id_fields[ $i ];
255 2 50       12 my $id_part = ref $id eq 'HASH' ? $id->{ $id_field } : $id;
256 2   50     14 $store = $store->{ $id_part } // return 0;
257             }
258 92 100       282 my $id_part = ref $id eq 'HASH' ? $id->{ $id_fields[-1] } : $id;
259 92 100       315 return 0 if !$store->{ $id_part };
260 70         421 return !!delete $store->{ $id_part };
261             }
262              
263             sub _normalize {
264 169     169   416 my ( $self, $schema_name, $data ) = @_;
265 169 50       450 return undef if !$data;
266 169         400 my $schema = $self->schema->{ $schema_name }{ properties };
267 169         325 my %replace;
268 169         610 for my $key ( keys %$data ) {
269 1035 100       9970 next if !defined $data->{ $key }; # leave nulls alone
270 819         1201 my ( $type, $format ) = @{ $schema->{ $key } }{qw( type format )};
  819         1989  
271 819 100 100     1861 if ( is_type( $type, 'boolean' ) ) {
    100          
272             # Boolean: true (1, "true"), false (0, "false")
273             $replace{ $key }
274 56 100 100     406 = $data->{ $key } && $data->{ $key } !~ /^false$/i
275             ? 1 : 0;
276             }
277             elsif ( is_type( $type, 'string' ) && is_format( $format, 'date-time' ) ) {
278 70 100       256 if ( $data->{ $key } eq 'now' ) {
279 57         398 $replace{ $key } = Time::Piece->new->datetime;
280             }
281             }
282             }
283 169         2719 +{ %$data, %replace };
284             }
285              
286             # Some databases can know other formats
287             my %db_formats = map { $_ => 1 } qw( date time date-time binary );
288              
289             sub read_schema {
290 104     104 0 74949 my ( $self, @table_names ) = @_;
291 104 50       440 my $schema = %Local::Test::SCHEMA ? \%Local::Test::SCHEMA : $self->schema;
292 104         29423 my $cloned = dclone $schema;
293 104         1150 delete @$cloned{@Local::Test::SCHEMA_ADDED_COLLS}; # ones not in the "database" at all
294             # zap all things that DB can't know about
295 104         503 for my $c ( values %$cloned ) {
296 463         788 delete $c->{'x-list-columns'};
297 463         623 for my $p ( values %{ $c->{properties} } ) {
  463         1304  
298 2890         4772 delete @$p{ qw(description pattern title) };
299 2890 100 100     6666 if ( $p->{format} && !$db_formats{ $p->{format} } ) {
300 399         749 delete $p->{format};
301             }
302             }
303             }
304 104 100       658 return @table_names ? @$cloned{ @table_names } : $cloned;
305             }
306              
307 1     1 0 7072 sub supports { grep { $_[1] eq $_ } 'complex-type' }
  1         6  
308              
309             1;
310              
311             __END__