File Coverage

blib/lib/WebService/Amazon/DynamoDB/Server.pm
Criterion Covered Total %
statement 192 227 84.5
branch 52 82 63.4
condition 12 25 48.0
subroutine 45 47 95.7
pod 17 20 85.0
total 318 401 79.3


line stmt bran cond sub pod time code
1             package WebService::Amazon::DynamoDB::Server;
2             # ABSTRACT: Perl implementation of the DynamoDB server API, for testing
3              
4 7     7   269572 use strict;
  7         14  
  7         242  
5 7     7   29 use warnings;
  7         10  
  7         282  
6              
7             our $VERSION = '0.001';
8              
9             =head1 NAME
10              
11             WebService::Amazon::DynamoDB - perl server implementation for the AWS DynamoDB API
12              
13             =head1 VERSION
14              
15             version 0.001
16              
17             =head1 DESCRIPTION
18              
19             =cut
20              
21 7     7   3221 use Mixin::Event::Dispatch::Bus;
  7         33653  
  7         180  
22              
23 7     7   3615 use Encode;
  7         58025  
  7         1172  
24 7     7   1387 use Future;
  7         9  
  7         154  
25 7     7   2246 use Future::Utils qw(call);
  7         8330  
  7         369  
26 7     7   33 use List::Util qw(min);
  7         10  
  7         509  
27 7     7   28 use List::UtilsBy qw(extract_by sort_by);
  7         7  
  7         360  
28 7     7   2852 use Time::Moment;
  7         7551  
  7         170  
29              
30 7     7   2880 use WebService::Amazon::DynamoDB::Server::Table;
  7         12  
  7         175  
31 7     7   37 use WebService::Amazon::DynamoDB::Server::Item;
  7         8  
  7         117  
32              
33 7     7   27 use constant LIST_TABLES_MAX => 100;
  7         9  
  7         16608  
34              
35             our %API_METHODS = map {; $_ => 1 } qw(
36             list_tables
37             create_table
38             delete_table
39             update_table
40             describe_table
41             get_item
42             put_item
43             );
44              
45             =head1 METHODS
46              
47             =cut
48              
49             =head2 new
50              
51             =cut
52              
53 7     7 1 470 sub new { my $class = shift; bless {@_}, $class }
  7         27  
54              
55             =head2 list_tables
56              
57             Takes the following named parameters:
58              
59             =over 4
60              
61             =item * ExclusiveStartTableName
62              
63             =item * Limit
64              
65             =back
66              
67             Resolves to a hashref containing the following data:
68              
69             =over 4
70              
71             =item * LastEvaluatedTableName
72              
73             =item * TableNames
74              
75             =back
76              
77             ListTables (p. 58)
78              
79             =cut
80              
81             sub list_tables {
82 5     5 1 5262 my ($self, %args) = @_;
83 5         17 my $req = { %args };
84              
85 5     30   21 my @tables = sort_by { $_->name } @{$self->{tables}};
  30         130  
  5         32  
86 5 100       125 if(exists $args{ExclusiveStartTableName}) {
87 3 100       14 return $self->fail(
88             list_tables => $req,
89             'ValidationException: table ' . $args{ExclusiveStartTableName} . ' not found',
90             ) unless $self->have_table($args{ExclusiveStartTableName});
91              
92 2   66     13 shift @tables while @tables && $tables[0]->name ne $args{ExclusiveStartTableName};
93             }
94 4   66     27 my $limit = min(LIST_TABLES_MAX, $args{Limit} // ());
95 4         6 my %result;
96 4 100       19 if(@tables > $limit) {
97 2         3 my ($last) = splice @tables, $limit;
98 2         5 $result{LastEvaluatedTableName} = $last->name;
99             }
100 4         15 $result{TableNames} = [ map $_->name, @tables ];
101 4         18 $self->done(list_tables => \%result, $req, \@tables)
102             }
103              
104             =head2 create_table
105              
106             CreateTable (p. 22)
107              
108             =cut
109              
110             sub create_table {
111 14     14 1 4627 my ($self, %args) = @_;
112 14         48 my $req = { %args };
113              
114 14 100       55 return $self->fail(
115             create_table => $req,
116             'ValidationException - no AttributeDefinitions found'
117             ) unless exists $args{AttributeDefinitions};
118              
119 13 100       40 return $self->fail(
120             create_table => $req,
121             'ValidationException - no KeySchema found'
122             ) unless exists $args{KeySchema};
123              
124 12         48 return $self->fail(
125             create_table => $req,
126             'ValidationException - empty KeySchema found'
127 12 100       15 ) unless @{$args{KeySchema}};
128              
129 11         39 return $self->fail(
130             create_table => $req,
131             'ValidationException - too many items found in KeySchema'
132 11 50       17 ) if @{$args{KeySchema}} > 2;
133              
134 11 100 50     61 return $self->fail(
135             create_table => $req,
136             'ValidationException - invalid KeyType, expected HASH'
137             ) unless ($args{KeySchema}[0]{KeyType} // '') eq 'HASH';
138              
139 10         39 return $self->fail(
140             create_table => $req,
141             'ValidationException - invalid KeyType, expected RANGE'
142 10 50 0     11 ) if @{$args{KeySchema}} > 1 && ($args{KeySchema}[1]{KeyType} // '') ne 'RANGE';
      33        
143              
144 10         17 my %attr = map {; $_->{AttributeName} => $_ } @{$args{AttributeDefinitions}};
  9         42  
  10         22  
145 10         59 return $self->fail(
146             create_table => $req,
147             'ValidationException - attribute ' . $_ . ' not found in AttributeDefinitions'
148 10         17 ) for grep !exists $attr{$_}, map $_->{AttributeName}, @{$args{KeySchema}};
149              
150 9 100       30 return $self->fail(
151             create_table => $req,
152             'ValidationException - no ProvisionedThroughput found'
153             ) unless exists $args{ProvisionedThroughput};
154              
155 8 100       23 return $self->fail(
156             create_table => $req,
157             'ValidationException - no ProvisionedThroughput found'
158             ) unless exists $args{TableName};
159              
160 7 100       25 return $self->fail(
161             create_table => $req,
162             'ResourceInUseException - this table exists already'
163             ) if $self->have_table($args{TableName});
164              
165 6         14 $args{TableStatus} = 'CREATING';
166 6         14 $args{ItemCount} = 0;
167 6         12 $args{TableSizeBytes} = 0;
168 6         844 $args{CreationDateTime} = Time::Moment->now;
169 6         44 my $tbl = $self->add_table(%args);
170 6         153 $self->done(create_table => {
171             TableDescription => {
172             %args,
173             CreationDateTime => $args{CreationDateTime}->to_string,
174             }
175             }, $req, $tbl);
176             }
177              
178             =head2 describe_table
179              
180             DescribeTable (p. 47)
181              
182             =cut
183              
184             sub describe_table {
185 9     9 1 14791 my ($self, %args) = @_;
186 9         25 my $req = { %args };
187              
188 9         20 my $name = delete $args{TableName};
189             $self->validate_table_state($name => 'ACTIVE')->then(sub {
190 5     5   365 my $tbl = $self->{table_map}{$name};
191 5         20 $self->done(describe_table => {
192             Table => $tbl
193             }, $req, $tbl)
194             }, sub {
195 4     4   271 $self->fail(describe_table => $req, @_)
196             })
197 9         23 }
198              
199             =head2 update_table
200              
201             UpdateTable (p. 119)
202              
203             =cut
204              
205             sub update_table {
206 3     3 1 1412 my ($self, %args) = @_;
207 3         8 my $req = { %args };
208              
209 3         11 my $name = delete $args{TableName};
210             $self->validate_table_state($name => 'ACTIVE')->then(sub {
211 1     1   60 my $tbl = $self->{table_map}{$name};
212 1         2 my %update;
213 1 50       3 if(my $throughput = delete $args{ProvisionedThroughput}) {
214 1         6 $update{ProvisionedThroughput}{$_} = $throughput->{$_} for grep exists $throughput->{$_}, qw(ReadCapacityUnits WriteCapacityUnits);
215             }
216 1 50       2 if(my $index = delete $args{GlobalSecondaryIndexUpdates}) {
217 0         0 $update{GlobalSecondaryIndexUpdates}{$_} = $index->{$_} for keys %$index;
218             }
219 1 50       3 return $self->fail(
220             update_table => $req,
221             'ValidationException - invalid keys provided'
222             ) if keys %args;
223 1         2 for my $k (keys %update) {
224 1         1 $tbl->{$k}{$_} = $update{$k}{$_} for keys %{$update{$k}};
  1         5  
225             }
226             $self->table_status($name => 'UPDATING')->then(sub {
227 1         54 $self->done(update_table => {
228             TableDescription => $tbl
229             }, $req, $tbl)
230             })
231 1         2 }, sub {
232 2     2   157 $self->fail(update_table => $req, @_)
233             })
234 3         5 }
235              
236             =head2 delete_table
237              
238             DeleteTable (p. 43)
239              
240             =cut
241              
242             sub delete_table {
243 4     4 1 1802 my ($self, %args) = @_;
244 4         12 my $req = { %args };
245              
246 4         8 my $name = delete $args{TableName};
247             $self->validate_table_state($name => qw(ACTIVE DELETING))->then(sub {
248 2 50   2   122 return $self->fail(
249             delete_table => $req,
250             'ValidationException - invalid keys provided'
251             ) if keys %args;
252 2         4 my $tbl = $self->{table_map}{$name};
253             $self->table_status($name => 'DELETING')->then(sub {
254 2         104 $self->done(delete_table => {
255             TableDescription => $tbl
256             }, $req, $tbl)
257             })
258 2         4 }, sub {
259 2     2   168 $self->fail(delete_table => $req, @_)
260             })
261 4         12 }
262              
263             =head2 put_item
264              
265             PutItem (p. 61)
266              
267             =cut
268              
269             sub put_item {
270 6     6 1 3068 my ($self, %args) = @_;
271 6         17 my $req = { %args };
272              
273 6         14 my $name = delete $args{TableName};
274             $self->validate_table_state($name => 'ACTIVE')->then(sub {
275 3     3   201 my $tbl = $self->{table_map}{$name};
276             $tbl->validate_id_for_item_data($args{Item})->then(sub {
277 2         111 my $id = shift;
278 2         6 my $new = !exists $self->{data}{$name}{$id};
279 2         9 my $item = $tbl->item_from_data(delete $args{Item});
280 2         5 $self->{data}{$name}{$id} = $item;
281              
282 2         14 my %result;
283             Future->needs_all(
284             $self->return_values(delete $args{ReturnValues}),
285             $self->consumed_capacity(delete $args{ReturnConsumedCapacity}),
286             $self->collection_metrics(delete $args{ReturnItemCollectionMetrics}),
287             )->then(sub {
288             # Only add the keys if they were requested
289 2         320 for(qw(Attributes ConsumedCapacity ItemCollectionMetrics)) {
290 6         7 my $k = shift;
291 6 100       15 $result{$_} = $k if defined $k
292             }
293              
294             # Commit the changes
295 2 100       6 ++$tbl->{ItemCount} if $new;
296 2         10 $tbl->{TableSizeBytes} += length Encode::decode('UTF-8', $id);
297              
298 2         278 $self->done(put_item => \%result, $req, $tbl, $item);
299             }, sub {
300 0         0 $self->fail(put_item => $req, @_)
301 2         8 });
302             }, sub {
303 1         67 $self->fail(put_item => $req, @_)
304             })
305 3         13 }, sub {
306 3     3   265 $self->fail(put_item => $req, @_)
307             })
308 6         15 }
309              
310             =head2 get_item
311              
312             GetItem (p. 52)
313              
314             =cut
315              
316             sub get_item {
317 4     4 1 1407 my ($self, %args) = @_;
318 4         16 my $req = { %args };
319              
320 4         8 my $name = delete $args{TableName};
321             $self->validate_table_state($name => 'ACTIVE')->then(sub {
322 1     1   66 my $tbl = $self->{table_map}{$name};
323             $tbl->validate_id_for_item_data($args{Key})->then(sub {
324 0         0 my $id = shift;
325 0         0 my %result;
326             my $item;
327 0 0       0 if(exists $self->{data}{$name}{$id}) {
328 0         0 $item = $result{Item} = $self->{data}{$name}{$id}
329             }
330             return $self->consumed_capacity(delete $args{ReturnConsumedCapacity})->then(sub {
331             # Only add the keys if they were requested
332 0         0 for(qw(ConsumedCapacity)) {
333 0         0 my $k = shift;
334 0 0       0 $result{$_} = $k if defined $k
335             }
336 0         0 return $self->done(get_item => \%result, $req, $tbl, $item);
337             }, sub {
338 0         0 $self->fail(get_item => $req, @_)
339             })
340 0         0 }, sub {
341 1         74 $self->fail(get_item => $req, @_)
342             })
343 1         8 }, sub {
344 3     3   309 $self->fail(get_item => $req, @_)
345             })
346 4         11 }
347              
348             =head2 update_item
349              
350             UpdateItem (p. 103)
351              
352             =cut
353              
354             sub update_item {
355 0     0 1 0 my ($self, %args) = @_;
356 0         0 my $req = { %args };
357              
358 0         0 my $name = delete $args{TableName};
359             $self->validate_table_state($name => 'ACTIVE')->then(sub {
360 0     0   0 my $tbl = $self->{table_map}{$name};
361             $tbl->validate_id_for_item_data($args{Key})->then(sub {
362 0         0 my $id = shift;
363 0         0 my %result;
364             my $item;
365 0 0       0 if(exists $self->{data}{$name}{$id}) {
366 0         0 $item = $result{Item} = $self->{data}{$name}{$id}
367             }
368             return $self->consumed_capacity(delete $args{ReturnConsumedCapacity})->then(sub {
369             # Only add the keys if they were requested
370 0         0 for(qw(ConsumedCapacity)) {
371 0         0 my $k = shift;
372 0 0       0 $result{$_} = $k if defined $k
373             }
374 0         0 return $self->done(update_item => \%result, $req, $item);
375             })
376 0         0 })
377 0         0 })
378 0         0 }
379              
380             =head2 METHODS - Internal
381              
382             The following methods are not part of the standard DynamoDB public API,
383             so they are not recommended for use directly.
384              
385             =cut
386              
387 133   66 133 0 753 sub bus { shift->{bus} //= Mixin::Event::Dispatch::Bus->new }
388              
389             =head2 add_table
390              
391             Adds this table - called by L if everything passes validation.
392              
393             =cut
394              
395             sub add_table {
396 12     12 1 35 my ($self, %args) = @_;
397 12 100       41 $args{TableName} = delete $args{name} if exists $args{name};
398 12         84 my $tbl = WebService::Amazon::DynamoDB::Server::Table->new(
399             %args
400             );
401 12         18 push @{$self->{tables}}, $tbl;
  12         32  
402 12         43 $self->{table_map}{$tbl->name} = $tbl;
403 12         30 $tbl
404             }
405              
406             =head2 drop_table
407              
408             Drops the table - called to remove a table that was previously in 'DELETING' state.
409              
410             =cut
411              
412             sub drop_table {
413 1     1 1 384 my ($self, %args) = @_;
414 1 50       5 $args{TableName} = delete $args{name} if exists $args{name};
415 1         2 my $name = $args{TableName};
416 1 50   1   4 extract_by { $_->name eq $name } @{$self->{tables}} or return Future->fail('table not found');
  1         10  
  1         6  
417 1 50       11 delete $self->{table_map}{$name} or return Future->fail('table not found in map');
418 1         15 Future->done
419             }
420              
421             =head2 return_values
422              
423             Resolves to the attributes requested for this update.
424              
425             =cut
426              
427             sub return_values {
428 2     2 1 2 my ($self, $v) = @_;
429 2 100 66     15 return Future->done(undef) if !defined($v) || $v eq 'NONE';
430 1 50       4 if($v eq 'ALL_OLD') {
431 1         3 return Future->done({ })
432             } else {
433 0         0 return Future->fail(
434             ValidationException =>
435             )
436             }
437             }
438              
439             =head2 consumed_capacity
440              
441             Returns consumed capacity information if available.
442              
443             =cut
444              
445             sub consumed_capacity {
446 2     2 1 24 my ($self, $v) = @_;
447 2 50 33     9 return Future->done(undef) if !defined($v) || $v eq 'NONE';
448 0 0       0 if($v eq 'INDEXES') {
    0          
449 0         0 return Future->done({ })
450             } elsif($v eq 'TOTAL') {
451 0         0 return Future->done({ })
452             } else {
453 0         0 return Future->fail(
454             ValidationException =>
455             )
456             }
457             }
458              
459             =head2 collection_metrics
460              
461             Resolves to collection metrics information, if available.
462              
463             =cut
464              
465             sub collection_metrics {
466 2     2 1 21 my ($self, $v) = @_;
467 2 50 33     10 return Future->done(undef) if !defined($v) || $v eq 'NONE';
468 0 0       0 if($v eq 'SIZE') {
469 0         0 return Future->done({ })
470             } else {
471 0         0 return Future->fail(
472             ValidationException =>
473             )
474             }
475             }
476              
477             my %valid_table_status = map {; $_ => 1 } qw(CREATING DELETING UPDATING ACTIVE);
478              
479             =head2 table_status
480              
481             Update or return current table status.
482              
483             =cut
484              
485             sub table_status {
486 11     11 1 4147 my ($self, $name, $status) = @_;
487 11 100       38 if(defined $status) {
488 9 50       26 return Future->fail('bad status') unless exists $valid_table_status{$status};
489 9         23 $self->{table_map}{$name}{TableStatus} = $status
490             }
491 11         39 Future->done($self->{table_map}{$name}{TableStatus});
492             }
493              
494             =head2 have_table
495              
496             Returns true if we have this table.
497              
498             =cut
499              
500             sub have_table {
501 39     39 1 7421 my ($self, $name) = @_;
502 39         197 return scalar exists $self->{table_map}{$name};
503             }
504              
505             =head2 validate_table_state
506              
507             Raises various exceptions based on table state.
508              
509             =cut
510              
511             sub validate_table_state {
512 26     26 1 52 my ($self, $name, @allowed) = @_;
513              
514 26 100       89 return Future->fail(
515             'ResourceNotFoundException'
516             ) unless defined $name;
517              
518 23 100       53 return Future->fail(
519             'ResourceNotFoundException'
520             ) unless $self->have_table($name);
521              
522 18         39 my $status = $self->{table_map}{$name}{TableStatus};
523 18 100       92 return Future->fail(
524             'ResourceInUseException'
525             ) unless grep $status eq $_, @allowed;
526              
527 12         39 Future->done;
528             }
529              
530             sub fail {
531 25     25 0 47 my ($self, $sub, $req, $exception, @details) = @_;
532 25 50       77 return Future->fail('invalid API name ' . $sub) unless exists $API_METHODS{$sub};
533 25         76 my $f = Future->fail($exception => @details);
534 25         374 $self->bus->invoke_event($sub => $req, $f);
535 25         7409 $self->bus->invoke_event(error => $req, $f);
536 25         464 $f
537             }
538              
539 7     7   43 use Carp qw(confess);
  7         8  
  7         1220  
540             sub done {
541 20     20 0 43 my ($self, $sub, $rslt, $req, @details) = @_;
542 20 50       52 confess 'wtf' unless defined $sub;
543 20 50       64 return Future->fail('invalid API name ' . $sub) unless exists $API_METHODS{$sub};
544 20         88 my $f = Future->done($rslt);
545 20         321 $self->bus->invoke_event($sub => $req, $f, @details);
546 20         3445 $f
547             }
548              
549             sub DESTROY {
550 7     7   5032 my ($self) = @_;
551 7         115 $self->bus->invoke_event(destroy => );
552             }
553              
554             1;
555              
556             __END__