File Coverage

blib/lib/SimpleDB/Client.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package SimpleDB::Client;
2             {
3             $SimpleDB::Client::VERSION = '1.0600';
4             }
5              
6             =head1 NAME
7              
8             SimpleDB::Client - The network interface to the SimpleDB service.
9              
10             =head1 VERSION
11              
12             version 1.0600
13              
14             =head1 SYNOPSIS
15              
16             use SimpleDB::Client;
17              
18             my $sdb = SimpleDB::Client->new(secret_key=>'abc', access_key=>'123');
19              
20             # create a domain
21             my $hashref = $sdb->send_request('CreateDomain', {DomainName => 'my_things'});
22              
23             # insert attributes
24             my $hashref = $sdb->send_request('PutAttributes', {
25             DomainName => 'my_things',
26             ItemName => 'car',
27             'Attribute.1.Name' => 'color',
28             'Attribute.1.Value' => 'red',
29             'Attribute.1.Replace' => 'true',
30             });
31              
32             # get attributes
33             my $hashref = $sdb->send_request('GetAttributes', {
34             DomainName => 'my_things',
35             ItemName => 'car',
36             });
37              
38             # search attributes
39             my $hashref = $sdb->send_request('Select', {
40             SelectExpression => q{select * from my_things where color = 'red'},
41             });
42              
43             =head1 DESCRIPTION
44              
45             This class will let you quickly and easily inteface with AWS SimpleDB. It throws exceptions from L<SimpleDB::Client::Exception>. It's very light weight. Although we haven't run any benchmarks on the other modules, it should outperform any of the other Perl modules that exist today.
46              
47             =head1 METHODS
48              
49             The following methods are available from this class.
50              
51             =cut
52              
53 1     1   1027 use Moose;
  0            
  0            
54             use Digest::SHA qw(hmac_sha256_base64);
55             use XML::Fast;
56             use LWP::UserAgent;
57             use HTTP::Request;
58             use Time::HiRes qw(usleep);
59             use URI::Escape qw(uri_escape_utf8);
60             use SimpleDB::Client::Exception;
61             use URI;
62              
63             #--------------------------------------------------------
64              
65             =head2 new ( params )
66              
67             =head3 params
68              
69             A hash containing the parameters to pass in to this method.
70              
71             =head4 access_key
72              
73             The access key given to you from Amazon when you sign up for the SimpleDB service at this URL: L<http://aws.amazon.com/simpledb/>
74              
75             =head4 secret_key
76              
77             The secret access key given to you from Amazon.
78              
79             =head4 simpledb_uri
80              
81             The constructor that SimpleDB::Client will connect to. Defaults to:
82            
83             URI->new('https://sdb.amazonaws.com/')
84              
85             =cut
86              
87             #--------------------------------------------------------
88              
89             =head2 access_key ( )
90              
91             Returns the access key passed to the constructor.
92              
93             =cut
94              
95             has 'access_key' => (
96             is => 'ro',
97             required => 1,
98             documentation => 'The AWS SimpleDB access key id provided by Amazon.',
99             );
100              
101             #--------------------------------------------------------
102              
103             =head2 secret_key ( )
104              
105             Returns the secret key passed to the constructor.
106              
107             =cut
108              
109             has 'secret_key' => (
110             is => 'ro',
111             required => 1,
112             documentation => 'The AWS SimpleDB secret access key id provided by Amazon.',
113             );
114              
115             #--------------------------------------------------------
116              
117             =head2 simpledb_uri ( )
118              
119             Returns the L<URI> object passed into the constructor that SimpleDB::Client will connect to. Defaults to:
120              
121             URI->new('https://sdb.amazonaws.com/')
122              
123             =cut
124              
125             has simpledb_uri => (
126             is => 'ro',
127             default => sub { URI->new('http://sdb.amazonaws.com/') },
128             );
129              
130             #--------------------------------------------------------
131              
132             =head2 user_agent ( )
133              
134             Returns the L<LWP::UserAgent> object that is used to connect to SimpleDB. It's cached here so it doesn't have to be created each time.
135              
136             =cut
137              
138             has user_agent => (
139             is => 'ro',
140             default => sub { LWP::UserAgent->new(timeout=>30, keep_alive=>1); },
141             );
142              
143             #--------------------------------------------------------
144              
145             =head2 construct_request ( action, [ params ] )
146              
147             Returns a string that contains the HTTP post data ready to make a request to SimpleDB. Normally this is only called by send_request(), but if you want to debug a SimpleDB interaction, then having access to this method is critical.
148              
149             =head3 action
150              
151             The action to perform on SimpleDB. See the "Operations" section of the guide located at L<http://docs.amazonwebservices.com/AmazonSimpleDB/2009-04-15/DeveloperGuide/>.
152              
153             =head3 params
154              
155             Any extra prameters required by the operation. The normal parameters of Action, AWSAccessKeyId, Version, Timestamp, SignatureMethod, SignatureVersion, and Signature are all automatically provided by this method.
156              
157             =cut
158              
159             sub construct_request {
160             my ($self, $action, $params) = @_;
161             my $encoding_pattern = "^A-Za-z0-9\-_.~";
162              
163             # add required parameters
164             $params->{'Action'} = $action;
165             $params->{'AWSAccessKeyId'} = $self->access_key;
166             $params->{'Version'} = '2009-04-15';
167             $params->{'Timestamp'} = sprintf("%04d-%02d-%02dT%02d:%02d:%02d.000Z", sub { ($_[5]+1900, $_[4]+1, $_[3], $_[2], $_[1], $_[0]) }->(gmtime(time)));
168             $params->{'SignatureMethod'} = 'HmacSHA256';
169             $params->{'SignatureVersion'} = 2;
170              
171             # construct post data
172             my $post_data;
173             foreach my $name (sort {$a cmp $b} keys %{$params}) {
174             $post_data .= $name . '=' . uri_escape_utf8($params->{$name}, $encoding_pattern) . '&';
175             }
176             chop $post_data;
177              
178             # sign the post data
179             my $signature = "POST\n".$self->simpledb_uri->host."\n/\n". $post_data;
180             $signature = hmac_sha256_base64($signature, $self->secret_key) . '=';
181             $post_data .= '&Signature=' . uri_escape_utf8($signature, $encoding_pattern);
182              
183             my $request = HTTP::Request->new('POST', $self->simpledb_uri->as_string);
184             $request->content_type("application/x-www-form-urlencoded; charset=utf-8");
185             $request->content($post_data);
186              
187             return $request;
188             }
189              
190             #--------------------------------------------------------
191              
192             =head2 send_request ( action, [ params ] )
193              
194             Creates a request, and then sends it to SimpleDB. The response is returned as a hash reference of the raw XML document returned by SimpleDB. Automatically attempts 5 cascading retries on connection failure.
195              
196             Throws SimpleDB::Client::Exception::Response and SimpleDB::Client::Exception::Connection.
197              
198             =head3 action
199              
200             See create_request() for details.
201              
202             =head3 params
203              
204             See create_request() for details.
205              
206             =cut
207              
208             sub send_request {
209             my ($self, $action, $params) = @_;
210             my $request = $self->construct_request($action, $params);
211             # loop til we get a response or throw an exception
212             foreach my $retry (1..5) {
213              
214             # make the request
215             my $ua = $self->user_agent;
216             my $response = $ua->request($request);
217              
218             # got a possibly recoverable error, let's retry
219             if ($response->code >= 500 && $response->code < 600) {
220             if ($retry < 5) {
221             usleep((4 ** $retry) * 100_000);
222             }
223             else {
224             warn $response->header('Reason');
225             SimpleDB::Client::Exception::Connection->throw(error=>'Exceeded maximum retries.', status_code=>$response->code);
226             }
227             }
228              
229             # not a retry
230             else {
231             return $self->handle_response($response);
232             }
233             }
234             }
235              
236             #--------------------------------------------------------
237              
238             =head2 handle_response ( response )
239              
240             Returns a hashref containing the response from SimpleDB.
241              
242             Throws SimpleDB::Client::Exception::Response.
243              
244             =head3 response
245              
246             The L<HTTP::Response> object created by the C<send_request> method.
247              
248             =cut
249              
250             sub handle_response {
251             my ($self, $response) = @_;
252             my $tree = eval { xml2hash($response->content) };
253             my (undef, $content) = each %$tree; # discard root like XMLin
254             # compatibility with SimpleDB::Class
255             if (exists $content->{SelectResult} && ! $content->{SelectResult}) {
256             $content->{SelectResult} = {};
257             }
258             # force an item list into an array
259             if (exists $content->{SelectResult}{Item} && ref $content->{SelectResult}{Item} ne 'ARRAY') {
260             $content->{SelectResult}{Item} = [ $content->{SelectResult}{Item} ];
261             }
262              
263             # choked reconstituing the XML, probably because it wasn't XML
264             if ($@) {
265             SimpleDB::Client::Exception::Response->throw(
266             error => 'Response was garbage. Confirm Net::SSLeay, XML::Parser, and XML::Simple installations.',
267             status_code => $response->code,
268             response => $response,
269             );
270             }
271              
272             # got a valid response
273             elsif ($response->is_success) {
274             return $content;
275             }
276              
277             # SimpleDB gave us an error message
278             else {
279             SimpleDB::Client::Exception::Response->throw(
280             error => $content->{Errors}{Error}{Message},
281             status_code => $response->code,
282             error_code => $content->{Errors}{Error}{Code},
283             box_usage => $content->{Errors}{Error}{BoxUsage},
284             request_id => $content->{RequestID},
285             response => $response,
286             );
287             }
288             }
289              
290             =head1 PREREQS
291              
292             This package requires the following modules:
293              
294             L<XML::Fast>
295             L<LWP>
296             L<Time::HiRes>
297             L<Crypt::SSLeay>
298             L<Moose>
299             L<Digest::SHA>
300             L<URI>
301             L<Exception::Class>
302              
303             =head1 SUPPORT
304              
305             =over
306              
307             =item Repository
308              
309             L<http://github.com/rizen/SimpleDB-Client>
310              
311             =item Bug Reports
312              
313             L<http://rt.cpan.org/Public/Dist/Display.html?Name=SimpleDB-Client>
314              
315             =back
316              
317             =head1 SEE ALSO
318              
319             There are other packages you can use to access SimpleDB. I chose not to use them because I wanted something a bit more lightweight that I could build L<SimpleDB::Class> on top of so I could easily map objects to SimpleDB Domain Items. If you're looking for a low level SimpleDB accessor and for some reason this module doesn't cut the mustard, then you should check out these:
320              
321             =over
322              
323             =item Amazon::SimpleDB (L<http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1136>)
324              
325             A complete and nicely functional low level library made by Amazon itself.
326              
327             =item L<Amazon::SimpleDB>
328              
329             A low level SimpleDB accessor that's in its infancy and may be abandoned, but appears to be pretty functional, and of the same scope as Amazon's own module.
330              
331             =back
332              
333             In addition to clients, there is at least one other API compatible server out there that basically lets you host your own SimpleDB if you don't want to put it in Amazon's cloud. It's called M/DB. You can read more about it here: L<http://gradvs1.mgateway.com/main/index.html?path=mdb>. Though I haven't tested it, since it's API compatible, you should be able to use it with both this module and L<SimpleDB::Class>.
334              
335             =head1 AUTHOR
336              
337             JT Smith <jt_at_plainblack_com>
338              
339             I have to give credit where credit is due: SimpleDB::Client is heavily inspired by the Amazon::SimpleDB class distributed by Amazon itself (not to be confused with L<Amazon::SimpleDB> written by Timothy Appnel).
340              
341             =head1 LEGAL
342              
343             SimpleDB::Client is Copyright 2009-2010 Plain Black Corporation (L<http://www.plainblack.com/>) and is licensed under the same terms as Perl itself.
344              
345             =cut
346              
347             no Moose;
348             __PACKAGE__->meta->make_immutable;