File Coverage

blib/lib/MooseX/Storage/IO/AmazonDynamoDB.pm
Criterion Covered Total %
statement 26 26 100.0
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 35 35 100.0


line stmt bran cond sub pod time code
1             package MooseX::Storage::IO::AmazonDynamoDB;
2              
3 1     1   661 use strict;
  1         2  
  1         24  
4 1     1   15 use 5.014;
  1         4  
5             our $VERSION = '0.07';
6              
7 1     1   472 use Data::Dumper;
  1         5156  
  1         53  
8 1     1   8 use JSON::MaybeXS;
  1         2  
  1         46  
9 1     1   413 use MooseX::Role::Parameterized;
  1         527895  
  1         11  
10 1     1   50209 use MooseX::Storage;
  1         31174  
  1         5  
11 1     1   872 use PawsX::DynamoDB::DocumentClient;
  1         478350  
  1         41  
12 1     1   606 use Types::Standard qw(HasMethods);
  1         53356  
  1         9  
13 1     1   703 use namespace::autoclean;
  1         3  
  1         10  
14              
15             parameter key_attr => (
16             isa => 'Str',
17             required => 1,
18             );
19              
20             parameter table_name => (
21             isa => 'Maybe[Str]',
22             default => undef,
23             );
24              
25             parameter table_name_method => (
26             isa => 'Str',
27             default => 'dynamo_db_table_name',
28             );
29              
30             parameter document_client_attribute_name => (
31             isa => 'Str',
32             default => 'dynamodb_document_client',
33             );
34              
35             parameter document_client_builder => (
36             isa => 'CodeRef',
37             default => sub { sub { PawsX::DynamoDB::DocumentClient->new() } },
38             );
39              
40             role {
41             my $p = shift;
42              
43             requires 'pack';
44             requires 'unpack';
45              
46             my $table_name_method = $p->table_name_method;
47             my $client_attr = $p->document_client_attribute_name;
48             my $client_builder = $p->document_client_builder;
49              
50             has $client_attr => (
51             is => 'ro',
52             isa => HasMethods[qw(get put)],
53             lazy => 1,
54             traits => [ 'DoNotSerialize' ],
55             default => $client_builder,
56             );
57              
58             method $table_name_method => sub {
59             my $class = ref $_[0] || $_[0];
60             return $p->table_name if $p->table_name;
61             die "$class: no table name defined!";
62             };
63              
64             method load => sub {
65             my ( $class, $item_key, %args ) = @_;
66             my $client = $args{dynamodb_document_client} || $client_builder->();
67             my $inject = $args{inject} || {};
68             my $table_name = $class->$table_name_method();
69              
70             my $packed = $client->get(
71             TableName => $table_name,
72             Key => {
73             $p->key_attr => $item_key,
74             },
75             ConsistentRead => 1,
76             );
77              
78             return undef unless $packed;
79              
80             # Deserialize JSON values
81             foreach my $key (keys %$packed) {
82             my $value = $packed->{$key};
83             if ($value && $value =~ /^\$json\$v(\d+)\$:(.+)$/) {
84             my ($version, $json) = ($1, $2);
85             state $coder = JSON::MaybeXS->new(
86             utf8 => 1,
87             canonical => 1,
88             allow_nonref => 1,
89             );
90             $packed->{$key} = $coder->decode($json);
91             }
92             }
93              
94             return $class->unpack(
95             $packed,
96             inject => {
97             %$inject,
98             $client_attr => $client,
99             }
100             );
101             };
102              
103             method store => sub {
104             my ( $self ) = @_;
105             my $client = $self->$client_attr;
106             my $table_name = $self->$table_name_method();
107             my $packed = $self->pack;
108             $client->put(
109             TableName => $table_name,
110             Item => $packed,
111             );
112             };
113             };
114              
115             1;
116             __END__
117              
118             =encoding utf-8
119              
120             =head1 NAME
121              
122             MooseX::Storage::IO::AmazonDynamoDB - Store and retrieve Moose objects to AWS's DynamoDB, via MooseX::Storage.
123              
124             =head1 SYNOPSIS
125              
126             First, create a table in DynamoDB. Currently only single-keyed tables are supported.
127              
128             aws dynamodb create-table \
129             --table-name my_docs \
130             --key-schema "AttributeName=doc_id,KeyType=HASH" \
131             --attribute-definitions "AttributeName=doc_id,AttributeType=S" \
132             --provisioned-throughput "ReadCapacityUnits=2,WriteCapacityUnits=2"
133              
134             Then, configure your Moose class via a call to Storage:
135              
136             package MyDoc;
137             use Moose;
138             use MooseX::Storage;
139              
140             with Storage(io => [ 'AmazonDynamoDB' => {
141             table_name => 'my_docs',
142             key_attr => 'doc_id',
143             }]);
144              
145             has 'doc_id' => (is => 'ro', isa => 'Str', required => 1);
146             has 'title' => (is => 'rw', isa => 'Str');
147             has 'body' => (is => 'rw', isa => 'Str');
148             has 'tags' => (is => 'rw', isa => 'ArrayRef');
149             has 'authors' => (is => 'rw', isa => 'HashRef');
150              
151             1;
152              
153             Now you can store/load your class to DyanmoDB:
154              
155             use MyDoc;
156              
157             # Create a new instance of MyDoc
158             my $doc = MyDoc->new(
159             doc_id => 'foo12',
160             title => 'Foo',
161             body => 'blah blah',
162             tags => [qw(horse yellow angry)],
163             authors => {
164             jdoe => {
165             name => 'John Doe',
166             email => 'jdoe@gmail.com',
167             roles => [qw(author reader)],
168             },
169             bsmith => {
170             name => 'Bob Smith',
171             email => 'bsmith@yahoo.com',
172             roles => [qw(editor reader)],
173             },
174             },
175             );
176              
177             # Save it to DynamoDB
178             $doc->store();
179              
180             # Load the saved data into a new instance
181             my $doc2 = MyDoc->load('foo12');
182              
183             # This should say 'Bob Smith'
184             print $doc2->authors->{bsmith}{name};
185              
186             =head1 DESCRIPTION
187              
188             MooseX::Storage::IO::AmazonDynamoDB is a Moose role that provides an io layer for L<MooseX::Storage> to store/load your Moose objects to Amazon's DynamoDB NoSQL database service.
189              
190             You should understand the basics of L<Moose>, L<MooseX::Storage>, and L<DynamoDB|http://aws.amazon.com/dynamodb/> before using this module.
191              
192             This module uses L<Paws> as its client library to the DynamoDB service, via L<PawsX::DynamoDB::DocumentClient>. By default it uses the Paws configuration defaults (region, credentials, etc.). You can customize this behavior - see L<"CLIENT CONFIGURATION">.
193              
194             At a bare minimum the consuming class needs to tell this role what table to use and what field to use as a primary key - see L<"table_name"> and L<"key_attr">.
195              
196             =head2 BREAKING CHANGES IN v0.07
197              
198             v0.07 transitioned the underlying DynamoDB client from L<Amazon::DynamoDB> to L<Paws::Dynamodb>, in order to stay more up-to-date with AWS features. Any existing code which customized the client configuration will break when upgrading to v0.07. Support for creating tables was also removed.
199              
200             The following role parameters were removed: client_attr, client_builder_method, client_class, client_args_method, host, port, ssl, dynamodb_local, create_table_method.
201              
202             The following attibutes were removed: dynamo_db_client
203              
204             The following methods were removed: build_dynamo_db_client, dynamo_db_client_args, dynamo_db_create_table
205              
206             The dynamo_db_client parameter to load() was removed, in favor of dynamodb_document_client.
207              
208             The dynamo_db_client and async parameters to store() were removed.
209              
210             Please see See L<"CLIENT CONFIGURATION"> for details on how to configure your client in v0.07 and above.
211              
212             =head1 PARAMETERS
213              
214             There are many parameters you can set when consuming this role that configure it in different ways.
215              
216             =head2 REQUIRED
217              
218             =head3 key_attr
219              
220             "key_attr" is a required parameter when consuming this role. It specifies an attribute in your class that will provide the primary key value for storing your object to DynamoDB. Currently only single primary keys are supported, or what DynamoDB calls "Hash Type Primary Key" (see their L<documentation|http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DataModel.html#DataModel.PrimaryKey>). See the L<"SYNOPSIS"> for an example.
221              
222             =head2 OPTIONAL
223              
224             =head3 table_name
225              
226             Specifies the name of the DynamoDB table to use for your objects - see the example in the L<"SYNOPSIS">. Alternatively, you can return the table name via a class method - see L<"dynamo_db_table_name">.
227              
228             =head3 table_name_method
229              
230             By default, this role will add a method named 'dynamo_db_table_name' to your class (see below for method description). If you want to use a different name for this method (e.g., because it conflicts with an existing method), you can change it via this parameter.
231              
232             =head3 document_client_attribute_name
233              
234             By default, this role adds an attribute to your class named 'dynamodb_document_client' (see below for attribute description). If you want to use a different name for this attribute, you can change it via this parameter.
235              
236             =head3 parameter document_client_builder
237              
238             Allows customization of the PawsX::DynamoDB::DocumentClient object used to interact with DynamoDB. See L<"CLIENT CONFIGURATION"> for more details.
239              
240             =head1 ATTRIBUTES
241              
242             =head2 dynamodb_document_client
243              
244             This role adds an attribute named "dynamodb_document_client" to your consuming class. This attribute holds an instance of L<PawsX::DynamoDB::DocumentClient> that will be used to communicate with the DynamoDB service.
245              
246             You can change this attribute's name via the document_client_attribute_name parameter.
247              
248             The attribute is lazily built via document_client_builder. See L<"CLIENT CONFIGURATION"> for more details.
249              
250             =head1 METHODS
251              
252             Following are methods that will be added to your consuming class.
253              
254             =head2 $obj->store()
255              
256             Object method. Stores the packed Moose object to DynamoDb.
257              
258             =head2 $obj = $class->load($key, [, dynamodb_document_client => $client ][, inject => { key => val, ... } ])
259              
260             Class method. Queries DynamoDB with a primary key, and returns a new Moose object built from the resulting data. Returns undefined if they key could not be found in DyanmoDB.
261              
262             The first argument is the primary key to use, and is required.
263              
264             Optional parameters can be specified following the key:
265              
266             =over 4
267              
268             =item dynamodb_document_client - Directly provide a PawsX::DynamoDB::DocumentClient object, instead of trying to build one using the class' configuration.
269              
270             =item inject - supply additional arguments to the class' new function, or override ones from the resulting data.
271              
272             =back
273              
274             =head2 dynamo_db_table_name
275              
276             A class method that will return the table name to use. This method will be called if the L<"table_name"> parameter is not set. So you could rewrite the Moose class in the L<"SYNOPSIS"> like this:
277              
278             package MyDoc;
279             use Moose;
280             use MooseX::Storage;
281              
282             with Storage(io => [ 'AmazonDynamoDB' => {
283             key_attr => 'doc_id',
284             }]);
285              
286             ...
287              
288             sub dynamo_db_table_name {
289             my $class = shift;
290             return $ENV{DEVELOPMENT} ? 'my_docs_dev' : 'my_docs';
291             }
292              
293             You can change this method's name via the table_name_method parameter.
294              
295             =head1 CLIENT CONFIGURATION
296              
297             This role uses the 'dynamodb_document_client' attribute (assuming you didn't rename it via 'document_client_attribute_name') to interact with DynamoDB. This attribute is lazily built, and should hold an instance of L<PawsX::DynamoDB::DocumentClient>.
298              
299             The client is built by a coderef that is stored in the role's document_client_builder parameter. By default, that coderef is simply:
300              
301             sub { return PawsX::DynamoDB::DocumentClient->new(); }
302              
303             If you need to customize the client, you do so by providing your own builder coderef. For instance, you could set the region directly:
304              
305             package MyDoc;
306             use Moose;
307             use MooseX::Storage;
308             use PawsX::DynamoDB::DocumentClient;
309              
310             with Storage(io => [ 'AmazonDynamoDB' => {
311             table_name => 'my_docs',
312             key_attr => 'doc_id',
313             document_client_builder => \&_build_document_client,
314             }]);
315              
316             sub _build_document_client {
317             my $region = get_my_region_somehow();
318             return PawsX::DynamoDB::DocumentClient->new(region => $region);
319             }
320              
321             See L<"DYNAMODB LOCAL"> for an example of configuring our Paws client to run against a locally running dynamodb clone.
322              
323             Note: the dynamodb_document_client attribute is not typed to a strict isa('PawsX::DynamoDB::DocumentClient'), but instead requires an object that has a 'get' and 'put' method. So you can provide some kind of mocked object, but that is left as an exercise to the reader - although examples are welcome!
324              
325             =head1 DYNAMODB LOCAL
326              
327             Here's an example of configuring your client to run against DynamoDB Local based on an environment variable. Make sure you've read L<CLIENT CONFIGURATION>. More information about DynamoDB Local can be found at L<AWS|http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html>.
328              
329             package MyDoc;
330             use Moose;
331             use MooseX::Storage;
332             use Paws;
333             use Paws::Credential::Explicit;
334             use PawsX::DynamoDB::DocumentClient;
335              
336             with Storage(io => [ 'AmazonDynamoDB' => {
337             table_name => $table_name,
338             key_attr => 'doc_id',
339             document_client_builder => \&_build_document_client,
340             }]);
341              
342             sub _build_document_client {
343             if ($ENV{DYNAMODB_LOCAL}) {
344             my $dynamodb = Paws->service(
345             'DynamoDB',
346             region => 'us-east-1',
347             region_rules => [ { uri => 'http://localhost:8000'} ],
348             credentials => Paws::Credential::Explicit->new(
349             access_key => 'XXXXXXXXX',
350             secret_key => 'YYYYYYYYY',
351             ),
352             max_attempts => 2,
353             );
354             return PawsX::DynamoDB::DocumentClient->new(dynamodb => $dynamodb);
355             }
356             return PawsX::DynamoDB::DocumentClient->new();
357             }
358              
359             =head1 NOTES
360              
361             =head2 Strongly consistent reads
362              
363             When executing load(), this module will always use strongly consistent reads when calling DynamoDB's GetItem operation. Read about DyanmoDB's consistency model in their L<FAQ|http://aws.amazon.com/dynamodb/faqs/> to learn more.
364              
365             =head2 Format level (freeze/thaw)
366              
367             Note that this role does not need you to implement a 'format' level for your object, i.e freeze/thaw. You can add one if you want it for other purposes.
368              
369             =head2 Pre-v0.07 objects
370              
371             Before v0.07, this module stored objects to DyanmoDB using L<Amazon::DynamoDB>. It worked around some issues with that module by serializing certain data types to JSON. Objects stored using this old system will be deserialized correctly.
372              
373             =head1 SEE ALSO
374              
375             =over 4
376              
377             =item L<Moose>
378              
379             =item L<MooseX::Storage>
380              
381             =item L<Amazon's DynamoDB Homepage|http://aws.amazon.com/dynamodb/>
382              
383             =item L<PawsX::DynamoDB::DocumentClient> - DynamoDB client.
384              
385             =item L<Paws> - AWS library.
386              
387             =back
388              
389             =head1 AUTHOR
390              
391             Steve Caldwell E<lt>scaldwell@gmail.comE<gt>
392              
393             =head1 COPYRIGHT
394              
395             Copyright 2015- Steve Caldwell E<lt>scaldwell@gmail.comE<gt>
396              
397             =head1 LICENSE
398              
399             This library is free software; you can redistribute it and/or modify
400             it under the same terms as Perl itself.
401              
402             =head1 ACKNOWLEDGEMENTS
403              
404             Thanks to L<Campus Explorer|http://www.campusexplorer.com>, who allowed me to release this code as open source.
405              
406             =cut