File Coverage

blib/lib/Yancy/Backend/Memory.pm
Criterion Covered Total %
statement 212 216 98.1
branch 94 106 88.6
condition 90 110 81.8
subroutine 20 20 100.0
pod 1 9 11.1
total 417 461 90.4


line stmt bran cond sub pod time code
1             package Yancy::Backend::Memory;
2             our $VERSION = '1.088';
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   20334 use Mojo::Base '-base';
  23         60  
  23         167  
15 23     23   4108 use List::Util qw( max );
  23         57  
  23         1580  
16 23     23   172 use Mojo::JSON qw( true false from_json to_json encode_json );
  23         48  
  23         1620  
17 23     23   151 use Mojo::File qw( path );
  23         50  
  23         1313  
18 23     23   176 use Storable qw( dclone );
  23         49  
  23         1198  
19 23     23   147 use Role::Tiny qw( with );
  23         83  
  23         244  
20             with 'Yancy::Backend::Role::Sync';
21 23     23   5268 use Yancy::Util qw( match is_type order_by is_format );
  23         51  
  23         1700  
22 23     23   13845 use Time::Piece;
  23         170943  
  23         115  
23              
24             our %DATA;
25              
26             sub new {
27 85     85 1 75552 my ( $class, $url, $schema ) = @_;
28 85 100       381 if ( $url ) {
29 84         536 my ( $path ) = $url =~ m{^[^:]+://[^/]+(?:/(.+))?$};
30 84 50       331 if ( $path ) {
31 0   0     0 %DATA = %{ from_json( path( ( $ENV{MOJO_HOME} || () ), $path )->slurp ) };
  0         0  
32             }
33             }
34 85   100     342 $schema //= \%Local::Test::SCHEMA;
35 85         566 return bless { init_arg => $url, schema => $schema }, $class;
36             }
37              
38             sub schema {
39 3210     3210 0 9812 my ( $self, $schema ) = @_;
40 3210 100       6691 if ( $schema ) {
41 52         160 $self->{schema} = $schema;
42 52         151 return;
43             }
44 3158         11287 $self->{schema};
45             }
46             sub collections;
47             *collections = *schema;
48              
49             sub create {
50 116     116 0 28766 my ( $self, $schema_name, $params ) = @_;
51 116         628 $params = { %$params };
52 116         412 my $props = $self->schema->{ $schema_name }{properties};
53             $params->{ $_ } = $props->{ $_ }{default} // undef
54 116   100     1517 for grep !exists $params->{ $_ },
55             keys %$props;
56 116         487 $params = $self->_normalize( $schema_name, $params ); # makes a copy
57              
58 116   100     405 my $id_field = $self->schema->{ $schema_name }{ 'x-id-field' } || 'id';
59 116 100       567 my @id_fields = ref $id_field eq 'ARRAY' ? @$id_field : ( $id_field );
60              
61             # Fill in any auto-increment data...
62 116         366 for my $id_field ( @id_fields ) {
63             # We haven't provided a value for an integer ID, assume it's autoinc
64 121 100 66     825 if ( !$params->{ $id_field } and $self->schema->{ $schema_name }{properties}{ $id_field }{type} eq 'integer' ) {
    100 66        
      66        
65 58         111 my @existing_ids = keys %{ $DATA{ $schema_name } };
  58         228  
66 58   100     489 $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         140  
  46         167  
74 46   100     323 $params->{id} = ( max( @existing_ids ) // 0 ) + 1;
75             }
76             }
77              
78 116   100     434 my $store = $DATA{ $schema_name } //= {};
79 116         387 for my $i ( 0 .. $#id_fields-1 ) {
80 5   100     37 $store = $store->{ $params->{ $id_fields[$i] } } //= {};
81             }
82 116         449 $store->{ $params->{ $id_fields[-1] } } = $params;
83              
84 116 100       655 return @id_fields > 1 ? { map {; $_ => $params->{ $_ } } @id_fields } : $params->{ $id_field };
  10         49  
85             }
86              
87             sub get {
88 328     328 0 418401 my ( $self, $schema_name, $id, %opt ) = @_;
89 328         935 my $schema = $self->schema->{ $schema_name };
90 328   100     2384 my $real_coll = ( $schema->{'x-view'} || {} )->{schema} // $schema_name;
      66        
91              
92 328   100     907 my $id_field = $self->schema->{ $schema_name }{ 'x-id-field' } || 'id';
93 328 100       1281 my @ids = ref $id_field eq 'ARRAY' ? map { $id->{ $_ } } @$id_field : ( $id );
  25         83  
94 327 100 66     1107 die "Missing composite ID parts" if @ids > 1 && ( !ref $id || keys %$id < @ids );
      100        
95              
96 326         777 my $item = $DATA{ $real_coll };
97 326         784 for my $id ( @ids ) {
98 337 100       975 return undef if !defined $id;
99 334   100     1412 $item = $item->{ $id } // return undef;
100             }
101              
102 281         975 $item = $self->_viewise( $schema_name, $item );
103 281 100       889 if ( my $join = $opt{join} ) {
104 4         16 $item = $self->_join( $schema_name, $item, $join );
105             }
106              
107 281         1461 return $item;
108             }
109              
110             sub _join {
111 21     21   52 my ( $self, $schema_name, $item, $join, $where ) = @_;
112 21         105 $item = { %$item };
113 21         53 my $schema = $self->schema->{ $schema_name };
114 21         34 my %props = %{ $schema->{properties} };
  21         75  
115 21   50     64 my $id_field = $schema->{ 'x-id-field' } || 'id';
116 21 100       60 my @joins = ref $join eq 'ARRAY' ? @$join : ( $join );
117 21         38 for my $join ( @joins ) {
118 29 100       74 if ( my $join_prop = $schema->{ properties }{ $join } ) {
    50          
119 21   100     51 my $join_id = $item->{ $join } || next;
120 17         27 my $join_schema_name = $join_prop->{'x-foreign-key'};
121 17         38 $item->{ $join } = $self->get( $join_schema_name, $join_id );
122 17         125 for my $key ( grep /^${join}\./, keys %$where ) {
123 7         80 my ( $k ) = $key =~ /^${join}\.(.+)$/;
124 7 100       35 if ( !match( { $k => $where->{ $key } }, $item->{ $join } ) ) {
125             # Inner match fails, so this row is not in the
126             # results
127 2         15 return;
128             }
129             }
130             }
131             elsif ( my $join_schema = $self->schema->{ $join } ) {
132 8         16 my $join_schema_name = $join;
133 8         19 my $join_props = $join_schema->{properties};
134             my $join_where = {
135 2         22 map { s/^$join\.//r => $where->{ $_ } }
136 8         24 grep { /^$join\./ }
  2         27  
137             keys %$where
138             };
139              
140             # First try to find the foreign key on the local schema
141 8 100 100     25 if ( my ( $join_prop_name ) = grep { ($props{ $_ }{ 'x-foreign-key' }//'') =~ /^$join_schema_name(\.|$)/ } keys %props ) {
  27 50       253  
142 3         8 my $join_prop = $props{ $join_prop_name };
143 3         11 my ( undef, $join_key_field ) = split /\./, $join_prop->{'x-foreign-key'};
144 3   50     21 $join_key_field //= $join_schema->{'x-id-field'} // 'id';
      33        
145             # Find the one foreign item
146 3         20 my $res = $self->list( $join_schema_name, { %$join_where, $join_key_field => $item->{ $join_prop_name } } );
147 3 50 33     15 return if keys %$join_where && !$res->{total};
148 3         14 $item->{ $join } = $res->{items}[0];
149             }
150             # Otherwise, try to find the foreign key on the foreign schema
151 20   100     112 elsif ( ( $join_prop_name ) = grep { ($join_props->{ $_ }{ 'x-foreign-key' }//'') =~ /^$schema_name(\.|$)/ } keys %$join_props ) {
152 5         12 my $join_prop = $join_props->{ $join_prop_name };
153 5         10 my $join_key_field;
154 5 50       19 if ( $join_prop->{'x-foreign-key'} =~ /\.(.+)$/ ) {
155 0         0 $join_key_field = $1;
156             }
157             else {
158 5   50     14 $join_key_field = $schema->{'x-id-field'} // 'id';
159             }
160             # Find the list of foreign items
161 5         33 my $res = $self->list( $join_schema_name, { %$join_where, $join_prop_name => $item->{ $join_key_field } } );
162 5 100 100     32 return if keys %$join_where && !$res->{total};
163 4         18 $item->{ $join } = $res->{items};
164             }
165             else {
166 0         0 die "Could not join $schema_name to $join: No x-foreign-key property found";
167             }
168             }
169             }
170 18         68 return $item;
171             }
172              
173             sub _viewise {
174 631     631   2020 my ( $self, $schema_name, $item, $join ) = @_;
175 631         23153 $item = dclone $item;
176 631         2347 my $schema = $self->schema->{ $schema_name };
177 631   100     3972 my $real_coll = ( $schema->{'x-view'} || {} )->{schema} // $schema_name;
      66        
178             my %props = %{
179 631         1245 $schema->{properties} || $self->schema->{ $real_coll }{properties}
180 631 100       4612 };
181 631 100       1877 if ( $join ) {
182 13 100       23 $props{ $_ } = 1 for @{ ref $join eq 'ARRAY' ? $join : [ $join ] };
  13         53  
183             }
184 631         4135 delete $item->{$_} for grep !$props{ $_ }, keys %$item;
185 631         3002 $item;
186             }
187              
188             sub list {
189 336     336 0 363605 my ( $self, $schema_name, $params, @opt ) = @_;
190 336 100       1341 my $opt = @opt % 2 == 0 ? {@opt} : $opt[0];
191 336         1067 my $schema = $self->schema->{ $schema_name };
192 336 50       978 die "list attempted on non-existent schema '$schema_name'" unless $schema;
193 336   100     1184 $params ||= {};
194              
195 336   100     741 my $id_field = $self->schema->{ $schema_name }{ 'x-id-field' } || 'id';
196 336 100       1178 my @id_fields = ref $id_field eq 'ARRAY' ? @$id_field : ( $id_field );
197              
198 336   100     2222 my $real_coll = ( $schema->{'x-view'} || {} )->{schema} // $schema_name;
      66        
199             my $props = $schema->{properties}
200 336   33     1185 || $self->schema->{ $real_coll }{properties};
201 336         628 my @rows = values %{ $DATA{ $real_coll } };
  336         1255  
202 336         1208 for my $id_field ( 1..$#id_fields ) {
203 9         89 @rows = map values %$_, @rows;
204             }
205 336 100       1065 if ( $opt->{join} ) {
206 7         24 @rows = map $self->_join( $schema_name, $_, $opt->{join}, $params ), @rows;
207             }
208             # Join queries have been resolved
209 336 100       1121 for my $p ( ref $params eq 'ARRAY' ? @$params : ( $params ) ) {
210 337         1394 for my $key ( grep /\./, keys %$p ) {
211 4         10 delete $p->{ $key };
212 4         14 my ( $j ) = split /\./, $key;
213 4         15 $p->{ $j } = { '!=' => undef };
214             }
215             }
216             my $matched_rows = order_by(
217             $opt->{order_by} // \@id_fields,
218 336   100     1747 [ grep { match( $params, $_ ) } @rows ],
  541         1521  
219             );
220 336   100     1447 my $first = $opt->{offset} // 0;
221 336 100       1018 my $last = $opt->{limit} ? $opt->{limit} + $first - 1 : $#$matched_rows;
222 336 100       929 if ( $last > $#$matched_rows ) {
223 58         114 $last = $#$matched_rows;
224             }
225 336         1656 my @items = map $self->_viewise( $schema_name, $_, $opt->{join} ), @$matched_rows[ $first .. $last ];
226 336         1355 my $retval = {
227             items => \@items,
228             total => scalar @$matched_rows,
229             };
230             #; use Data::Dumper;
231             #; say Dumper $retval;
232 336         1784 return $retval;
233             }
234              
235             sub set {
236 58     58 0 46962 my ( $self, $schema_name, $id, $params ) = @_;
237 58   100     192 my $id_field = $self->schema->{ $schema_name }{ 'x-id-field' } || 'id';
238 58 100       269 my @id_fields = ref $id_field eq 'ARRAY' ? @$id_field : ( $id_field );
239 58 100 100     342 die "Missing composite ID parts" if @id_fields > 1 && ( !ref $id || keys %$id < @id_fields );
      100        
240              
241             # Fill in any missing params from the ID
242 56         166 for my $id_field ( @id_fields ) {
243 58 100       202 my $id_part = ref $id eq 'HASH' ? $id->{ $id_field } : $id;
244 58 100       241 if ( !$params->{ $id_field } ) {
245 34         154 $params->{ $id_field } = $id_part;
246             }
247             }
248              
249 56         232 $params = $self->_normalize( $schema_name, $params );
250              
251 56         183 my $store = $DATA{ $schema_name };
252 56         249 for my $i ( 0..$#id_fields-1 ) {
253 2         9 my $id_field = $id_fields[ $i ];
254 2 50       18 my $id_part = ref $id eq 'HASH' ? $id->{ $id_field } : $id;
255 2 50       11 return 0 if !$store->{ $id_part };
256             # Update the item's ID if it changes
257 2         6 my $item = delete $store->{ $id_part };
258 2         7 $store->{ $params->{ $id_field } } = $item;
259 2         9 $store = $item;
260             }
261 56 100       237 my $id_part = ref $id eq 'HASH' ? $id->{ $id_fields[-1] } : $id;
262 56 100       240 return 0 if !$store->{ $id_part };
263             $store->{ $params->{ $id_fields[-1] } } = {
264 46         103 %{ delete $store->{ $id_part } },
  46         609  
265             %$params,
266             };
267              
268 46         368 return 1;
269             }
270              
271             sub delete {
272 94     94 0 968424 my ( $self, $schema_name, $id ) = @_;
273 94 50       356 return 0 if !$id;
274 94   100     252 my $id_field = $self->schema->{ $schema_name }{ 'x-id-field' } || 'id';
275 94 100       355 my @id_fields = ref $id_field eq 'ARRAY' ? @$id_field : ( $id_field );
276 94 100 100     321 die "Missing composite ID parts" if @id_fields > 1 && ( !ref $id || keys %$id < @id_fields );
      100        
277 92         220 my $store = $DATA{ $schema_name };
278 92         316 for my $i ( 0..$#id_fields-1 ) {
279 2         7 my $id_field = $id_fields[ $i ];
280 2 50       12 my $id_part = ref $id eq 'HASH' ? $id->{ $id_field } : $id;
281 2   50     13 $store = $store->{ $id_part } // return 0;
282             }
283 92 100       276 my $id_part = ref $id eq 'HASH' ? $id->{ $id_fields[-1] } : $id;
284 92 100       377 return 0 if !$store->{ $id_part };
285 70         489 return !!delete $store->{ $id_part };
286             }
287              
288             sub _normalize {
289 172     172   467 my ( $self, $schema_name, $data ) = @_;
290 172 50       498 return undef if !$data;
291 172         402 my $schema = $self->schema->{ $schema_name }{ properties };
292 172         367 my %replace;
293 172         653 for my $key ( keys %$data ) {
294 1044 100       10091 next if !defined $data->{ $key }; # leave nulls alone
295 825         1179 my ( $type, $format ) = @{ $schema->{ $key } }{qw( type format )};
  825         2078  
296 825 100 100     1930 if ( is_type( $type, 'boolean' ) ) {
    100          
297             # Boolean: true (1, "true"), false (0, "false")
298             $replace{ $key }
299 56 100 100     419 = $data->{ $key } && $data->{ $key } !~ /^false$/i
300             ? 1 : 0;
301             }
302             elsif ( is_type( $type, 'string' ) && is_format( $format, 'date-time' ) ) {
303 70 100       238 if ( $data->{ $key } eq 'now' ) {
304 57         361 $replace{ $key } = Time::Piece->new->datetime;
305             }
306             }
307             }
308 172         2930 +{ %$data, %replace };
309             }
310              
311             # Some databases can know other formats
312             my %db_formats = map { $_ => 1 } qw( date time date-time binary );
313              
314             sub read_schema {
315 106     106 0 70927 my ( $self, @table_names ) = @_;
316 106 50       442 my $schema = %Local::Test::SCHEMA ? \%Local::Test::SCHEMA : $self->schema;
317 106         32482 my $cloned = dclone $schema;
318 106         1151 delete @$cloned{@Local::Test::SCHEMA_ADDED_COLLS}; # ones not in the "database" at all
319             # zap all things that DB can't know about
320 106         536 for my $c ( values %$cloned ) {
321 545         957 delete $c->{'x-list-columns'};
322 545         755 for my $p ( values %{ $c->{properties} } ) {
  545         1439  
323 3150         4943 delete @$p{ qw(description pattern title) };
324 3150 100 100     7424 if ( $p->{format} && !$db_formats{ $p->{format} } ) {
325 413         774 delete $p->{format};
326             }
327             }
328             }
329 106 100       714 return @table_names ? @$cloned{ @table_names } : $cloned;
330             }
331              
332 1     1 0 6288 sub supports { grep { $_[1] eq $_ } 'complex-type' }
  1         6  
333              
334             1;
335              
336             __END__