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.087';
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   22898 use Mojo::Base '-base';
  23         63  
  23         242  
15 23     23   4760 use List::Util qw( max );
  23         53  
  23         1699  
16 23     23   201 use Mojo::JSON qw( true false from_json to_json encode_json );
  23         60  
  23         1736  
17 23     23   168 use Mojo::File qw( path );
  23         53  
  23         1274  
18 23     23   174 use Storable qw( dclone );
  23         54  
  23         1335  
19 23     23   157 use Role::Tiny qw( with );
  23         71  
  23         254  
20             with 'Yancy::Backend::Role::Sync';
21 23     23   5637 use Yancy::Util qw( match is_type order_by is_format );
  23         51  
  23         1629  
22 23     23   15210 use Time::Piece;
  23         179718  
  23         139  
23              
24             our %DATA;
25              
26             sub new {
27 84     84 1 83577 my ( $class, $url, $schema ) = @_;
28 84 100       315 if ( $url ) {
29 83         526 my ( $path ) = $url =~ m{^[^:]+://[^/]+(?:/(.+))?$};
30 83 50       343 if ( $path ) {
31 0   0     0 %DATA = %{ from_json( path( ( $ENV{MOJO_HOME} || () ), $path )->slurp ) };
  0         0  
32             }
33             }
34 84   100     324 $schema //= \%Local::Test::SCHEMA;
35 84         582 return bless { init_arg => $url, schema => $schema }, $class;
36             }
37              
38             sub schema {
39 3184     3184 0 10340 my ( $self, $schema ) = @_;
40 3184 100       6968 if ( $schema ) {
41 51         179 $self->{schema} = $schema;
42 51         142 return;
43             }
44 3133         11292 $self->{schema};
45             }
46             sub collections;
47             *collections = *schema;
48              
49             sub create {
50 113     113 0 31148 my ( $self, $schema_name, $params ) = @_;
51 113         662 $params = { %$params };
52 113         445 my $props = $self->schema->{ $schema_name }{properties};
53             $params->{ $_ } = $props->{ $_ }{default} // undef
54 113   100     1519 for grep !exists $params->{ $_ },
55             keys %$props;
56 113         509 $params = $self->_normalize( $schema_name, $params ); # makes a copy
57              
58 113   100     388 my $id_field = $self->schema->{ $schema_name }{ 'x-id-field' } || 'id';
59 113 100       461 my @id_fields = ref $id_field eq 'ARRAY' ? @$id_field : ( $id_field );
60              
61             # Fill in any auto-increment data...
62 113         276 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     902 if ( !$params->{ $id_field } and $self->schema->{ $schema_name }{properties}{ $id_field }{type} eq 'integer' ) {
    100 66        
      66        
65 55         121 my @existing_ids = keys %{ $DATA{ $schema_name } };
  55         237  
66 55   100     483 $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         100 my @existing_ids = map { $_->{ id } } values %{ $DATA{ $schema_name } };
  45         141  
  46         166  
74 46   100     364 $params->{id} = ( max( @existing_ids ) // 0 ) + 1;
75             }
76             }
77              
78 113   100     432 my $store = $DATA{ $schema_name } //= {};
79 113         380 for my $i ( 0 .. $#id_fields-1 ) {
80 5   100     61 $store = $store->{ $params->{ $id_fields[$i] } } //= {};
81             }
82 113         452 $store->{ $params->{ $id_fields[-1] } } = $params;
83              
84 113 100       641 return @id_fields > 1 ? { map {; $_ => $params->{ $_ } } @id_fields } : $params->{ $id_field };
  10         103  
85             }
86              
87             sub get {
88 327     327 0 436203 my ( $self, $schema_name, $id, %opt ) = @_;
89 327         1031 my $schema = $self->schema->{ $schema_name };
90 327   100     2430 my $real_coll = ( $schema->{'x-view'} || {} )->{schema} // $schema_name;
      66        
91              
92 327   100     883 my $id_field = $self->schema->{ $schema_name }{ 'x-id-field' } || 'id';
93 327 100       1321 my @ids = ref $id_field eq 'ARRAY' ? map { $id->{ $_ } } @$id_field : ( $id );
  25         88  
94 326 100 66     1136 die "Missing composite ID parts" if @ids > 1 && ( !ref $id || keys %$id < @ids );
      100        
95              
96 325         784 my $item = $DATA{ $real_coll };
97 325         1064 for my $id ( @ids ) {
98 336 100       1075 return undef if !defined $id;
99 333   100     1453 $item = $item->{ $id } // return undef;
100             }
101              
102 280         911 $item = $self->_viewise( $schema_name, $item );
103 280 100       927 if ( my $join = $opt{join} ) {
104 3         15 $item = $self->_join( $schema_name, $item, $join );
105             }
106              
107 280         1601 return $item;
108             }
109              
110             sub _join {
111 19     19   50 my ( $self, $schema_name, $item, $join, $where ) = @_;
112 19         90 $item = { %$item };
113 19         40 my $schema = $self->schema->{ $schema_name };
114 19   50     34 my $id_field = $self->schema->{ $schema_name }{ 'x-id-field' } || 'id';
115 19 100       56 my @joins = ref $join eq 'ARRAY' ? @$join : ( $join );
116 19         37 for my $join ( @joins ) {
117 26 100       70 if ( my $join_prop = $schema->{ properties }{ $join } ) {
    50          
118 21   100     58 my $join_id = $item->{ $join } || next;
119 17         27 my $join_schema_name = $join_prop->{'x-foreign-key'};
120 17         42 $item->{ $join } = $self->get( $join_schema_name, $join_id );
121 17         141 for my $key ( grep /^${join}\./, keys %$where ) {
122 7         74 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         9 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         76  
  5         19  
133 5 50       33 my $join_where = ref $id_field eq 'ARRAY' ? { map { $_ => $item->{ $_ } } @$join_id_field } : { $join_id_field => $item->{$join_id_field} };
  0         0  
134 5         8 my $min_items = 0;
135 5         40 for my $key ( grep /^${join}\./, keys %$where ) {
136 2         22 my ( $k ) = $key =~ /^${join}\.(.+)$/;
137 2         7 $join_where->{ $k } = $where->{ $key };
138 2         6 $min_items = 1;
139             }
140 5         26 my $res = $self->list( $join_schema_name, $join_where );
141 5 100       21 return if $res->{total} < $min_items;
142 4         18 $item->{ $join } = $res->{items};
143             }
144             }
145 16         60 return $item;
146             }
147              
148             sub _viewise {
149 626     626   2081 my ( $self, $schema_name, $item, $join ) = @_;
150 626         24957 $item = dclone $item;
151 626         2322 my $schema = $self->schema->{ $schema_name };
152 626   100     4046 my $real_coll = ( $schema->{'x-view'} || {} )->{schema} // $schema_name;
      66        
153             my %props = %{
154 626         1344 $schema->{properties} || $self->schema->{ $real_coll }{properties}
155 626 100       4931 };
156 626 100       1960 if ( $join ) {
157 12 100       18 $props{ $_ } = 1 for @{ ref $join eq 'ARRAY' ? $join : [ $join ] };
  12         51  
158             }
159 626         4250 delete $item->{$_} for grep !$props{ $_ }, keys %$item;
160 626         2971 $item;
161             }
162              
163             sub list {
164 327     327 0 374686 my ( $self, $schema_name, $params, @opt ) = @_;
165 327 100       1351 my $opt = @opt % 2 == 0 ? {@opt} : $opt[0];
166 327         1166 my $schema = $self->schema->{ $schema_name };
167 327 50       1008 die "list attempted on non-existent schema '$schema_name'" unless $schema;
168 327   100     1270 $params ||= {};
169              
170 327   100     777 my $id_field = $self->schema->{ $schema_name }{ 'x-id-field' } || 'id';
171 327 100       1245 my @id_fields = ref $id_field eq 'ARRAY' ? @$id_field : ( $id_field );
172              
173 327   100     2245 my $real_coll = ( $schema->{'x-view'} || {} )->{schema} // $schema_name;
      66        
174             my $props = $schema->{properties}
175 327   33     1236 || $self->schema->{ $real_coll }{properties};
176 327         606 my @rows = values %{ $DATA{ $real_coll } };
  327         1284  
177 327         1209 for my $id_field ( 1..$#id_fields ) {
178 8         72 @rows = map values %$_, @rows;
179             }
180 327 100       1086 if ( $opt->{join} ) {
181 6         26 @rows = map $self->_join( $schema_name, $_, $opt->{join}, $params ), @rows;
182             }
183             # Join queries have been resolved
184 327 100       1159 for my $p ( ref $params eq 'ARRAY' ? @$params : ( $params ) ) {
185 328         1412 for my $key ( grep /\./, keys %$p ) {
186 4         9 delete $p->{ $key };
187 4         15 my ( $j ) = split /\./, $key;
188 4         16 $p->{ $j } = { '!=' => undef };
189             }
190             }
191             my $matched_rows = order_by(
192             $opt->{order_by} // \@id_fields,
193 327   100     1751 [ grep { match( $params, $_ ) } @rows ],
  537         1712  
194             );
195 327   100     1453 my $first = $opt->{offset} // 0;
196 327 100       1068 my $last = $opt->{limit} ? $opt->{limit} + $first - 1 : $#$matched_rows;
197 327 100       994 if ( $last > $#$matched_rows ) {
198 57         115 $last = $#$matched_rows;
199             }
200 327         1799 my @items = map $self->_viewise( $schema_name, $_, $opt->{join} ), @$matched_rows[ $first .. $last ];
201 327         1373 my $retval = {
202             items => \@items,
203             total => scalar @$matched_rows,
204             };
205             #; use Data::Dumper;
206             #; say Dumper $retval;
207 327         1870 return $retval;
208             }
209              
210             sub set {
211 58     58 0 49231 my ( $self, $schema_name, $id, $params ) = @_;
212 58   100     231 my $id_field = $self->schema->{ $schema_name }{ 'x-id-field' } || 'id';
213 58 100       289 my @id_fields = ref $id_field eq 'ARRAY' ? @$id_field : ( $id_field );
214 58 100 100     263 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         169 for my $id_field ( @id_fields ) {
218 58 100       205 my $id_part = ref $id eq 'HASH' ? $id->{ $id_field } : $id;
219 58 100       298 if ( !$params->{ $id_field } ) {
220 34         168 $params->{ $id_field } = $id_part;
221             }
222             }
223              
224 56         257 $params = $self->_normalize( $schema_name, $params );
225              
226 56         176 my $store = $DATA{ $schema_name };
227 56         230 for my $i ( 0..$#id_fields-1 ) {
228 2         17 my $id_field = $id_fields[ $i ];
229 2 50       18 my $id_part = ref $id eq 'HASH' ? $id->{ $id_field } : $id;
230 2 50       13 return 0 if !$store->{ $id_part };
231             # Update the item's ID if it changes
232 2         8 my $item = delete $store->{ $id_part };
233 2         8 $store->{ $params->{ $id_field } } = $item;
234 2         8 $store = $item;
235             }
236 56 100       214 my $id_part = ref $id eq 'HASH' ? $id->{ $id_fields[-1] } : $id;
237 56 100       260 return 0 if !$store->{ $id_part };
238             $store->{ $params->{ $id_fields[-1] } } = {
239 46         113 %{ delete $store->{ $id_part } },
  46         605  
240             %$params,
241             };
242              
243 46         344 return 1;
244             }
245              
246             sub delete {
247 94     94 0 834886 my ( $self, $schema_name, $id ) = @_;
248 94 50       371 return 0 if !$id;
249 94   100     285 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     329 die "Missing composite ID parts" if @id_fields > 1 && ( !ref $id || keys %$id < @id_fields );
      100        
252 92         219 my $store = $DATA{ $schema_name };
253 92         312 for my $i ( 0..$#id_fields-1 ) {
254 2         7 my $id_field = $id_fields[ $i ];
255 2 50       12 my $id_part = ref $id eq 'HASH' ? $id->{ $id_field } : $id;
256 2   50     12 $store = $store->{ $id_part } // return 0;
257             }
258 92 100       280 my $id_part = ref $id eq 'HASH' ? $id->{ $id_fields[-1] } : $id;
259 92 100       338 return 0 if !$store->{ $id_part };
260 70         466 return !!delete $store->{ $id_part };
261             }
262              
263             sub _normalize {
264 169     169   467 my ( $self, $schema_name, $data ) = @_;
265 169 50       503 return undef if !$data;
266 169         408 my $schema = $self->schema->{ $schema_name }{ properties };
267 169         374 my %replace;
268 169         679 for my $key ( keys %$data ) {
269 1035 100       10997 next if !defined $data->{ $key }; # leave nulls alone
270 819         1189 my ( $type, $format ) = @{ $schema->{ $key } }{qw( type format )};
  819         2108  
271 819 100 100     1963 if ( is_type( $type, 'boolean' ) ) {
    100          
272             # Boolean: true (1, "true"), false (0, "false")
273             $replace{ $key }
274 56 100 100     401 = $data->{ $key } && $data->{ $key } !~ /^false$/i
275             ? 1 : 0;
276             }
277             elsif ( is_type( $type, 'string' ) && is_format( $format, 'date-time' ) ) {
278 70 100       272 if ( $data->{ $key } eq 'now' ) {
279 57         402 $replace{ $key } = Time::Piece->new->datetime;
280             }
281             }
282             }
283 169         2503 +{ %$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 73684 my ( $self, @table_names ) = @_;
291 104 50       470 my $schema = %Local::Test::SCHEMA ? \%Local::Test::SCHEMA : $self->schema;
292 104         30318 my $cloned = dclone $schema;
293 104         1113 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         493 for my $c ( values %$cloned ) {
296 463         816 delete $c->{'x-list-columns'};
297 463         669 for my $p ( values %{ $c->{properties} } ) {
  463         1300  
298 2890         4557 delete @$p{ qw(description pattern title) };
299 2890 100 100     6854 if ( $p->{format} && !$db_formats{ $p->{format} } ) {
300 399         730 delete $p->{format};
301             }
302             }
303             }
304 104 100       699 return @table_names ? @$cloned{ @table_names } : $cloned;
305             }
306              
307 1     1 0 6218 sub supports { grep { $_[1] eq $_ } 'complex-type' }
  1         7  
308              
309             1;
310              
311             __END__